Merge "Remove RestrictTo from internal items in navigation and fragment" into androidx-main
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 5ea093a..6e9c5ba 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -52,6 +52,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.StyleableRes;
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.R;
 import androidx.appcompat.app.AlertDialog;
@@ -80,6 +81,8 @@
 @AppCompatShadowedAttributes
 public class AppCompatSpinner extends Spinner implements TintableBackgroundView {
 
+    @SuppressLint("ResourceType")
+    @StyleableRes
     private static final int[] ATTRS_ANDROID_SPINNERMODE = {android.R.attr.spinnerMode};
 
     private static final int MAX_ITEMS_MEASURED = 15;
diff --git a/appsearch/appsearch-builtin-types/api/current.txt b/appsearch/appsearch-builtin-types/api/current.txt
index e867102c..e387d30 100644
--- a/appsearch/appsearch-builtin-types/api/current.txt
+++ b/appsearch/appsearch-builtin-types/api/current.txt
@@ -12,16 +12,94 @@
 
 package androidx.appsearch.builtintypes {
 
-  @androidx.appsearch.annotation.Document public class Timer {
+  @androidx.appsearch.annotation.Document(name="builtin:Alarm") public class Alarm {
+    method public long getBlackoutEndTimeMillis();
+    method public long getBlackoutStartTimeMillis();
+    method public long getCreationTimestampMillis();
+    method public int[]? getDaysOfWeek();
+    method @IntRange(from=0, to=23) public int getHour();
+    method public String getId();
+    method @IntRange(from=0, to=59) public int getMinute();
+    method public String? getName();
+    method public String getNamespace();
+    method public androidx.appsearch.builtintypes.AlarmInstance? getNextInstance();
+    method public androidx.appsearch.builtintypes.AlarmInstance? getPreviousInstance();
+    method public String? getRingtone();
+    method public int getScore();
+    method public long getTtlMillis();
+    method public boolean isEnabled();
+    method public boolean isVibrate();
+  }
+
+  public static final class Alarm.Builder {
+    ctor public Alarm.Builder(String, String);
+    ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
+    method public androidx.appsearch.builtintypes.Alarm build();
+    method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutEndTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutStartTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setDaysOfWeek(@IntRange(from=java.util.Calendar.SUNDAY, to=java.util.Calendar.SATURDAY) int...);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setEnabled(boolean);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setHour(@IntRange(from=0, to=23) int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setMinute(@IntRange(from=0, to=59) int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setName(String?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setNextInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setPreviousInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setRingtone(String?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setScore(int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setTtlMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setVibrate(boolean);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:AlarmInstance") public class AlarmInstance {
+    method public long getCreationTimestampMillis();
+    method @IntRange(from=1, to=31) public int getDay();
+    method @IntRange(from=0, to=23) public int getHour();
+    method public String getId();
+    method @IntRange(from=0, to=59) public int getMinute();
+    method @IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) public int getMonth();
+    method public String getNamespace();
+    method public int getScore();
+    method public long getSnoozeDurationMillis();
+    method public int getStatus();
+    method public long getTtlMillis();
+    method public int getYear();
+    field public static final int STATUS_DISMISSED = 3; // 0x3
+    field public static final int STATUS_FIRING = 2; // 0x2
+    field public static final int STATUS_MISSED = 5; // 0x5
+    field public static final int STATUS_SCHEDULED = 1; // 0x1
+    field public static final int STATUS_SNOOZED = 4; // 0x4
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class AlarmInstance.Builder {
+    ctor public AlarmInstance.Builder(String, String);
+    ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
+    method public androidx.appsearch.builtintypes.AlarmInstance build();
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDay(@IntRange(from=1, to=31) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setHour(@IntRange(from=0, to=23) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMinute(@IntRange(from=0, to=59) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMonth(@IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setScore(int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setSnoozeDurationMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setStatus(int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setTtlMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setYear(int);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:Timer") public class Timer {
+    method public long getCreationTimestampMillis();
     method public long getDurationMillis();
-    method public long getExpireTimeMillis();
     method public String getId();
     method public String? getName();
     method public String getNamespace();
     method public long getRemainingTimeMillis();
     method public String? getRingtone();
     method public int getScore();
-    method public int getTimerStatus();
+    method public long getStartTimeMillis();
+    method public long getStartTimeMillisInElapsedRealtime();
+    method public int getStatus();
     method public long getTtlMillis();
     method public boolean isVibrate();
     field public static final int STATUS_EXPIRED = 3; // 0x3
@@ -34,14 +112,17 @@
 
   public static final class Timer.Builder {
     ctor public Timer.Builder(String, String);
+    ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
     method public androidx.appsearch.builtintypes.Timer build();
+    method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setDurationMillis(long);
-    method public androidx.appsearch.builtintypes.Timer.Builder setExpireTimeMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setName(String?);
     method public androidx.appsearch.builtintypes.Timer.Builder setRemainingTimeMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setRingtone(String?);
     method public androidx.appsearch.builtintypes.Timer.Builder setScore(int);
-    method public androidx.appsearch.builtintypes.Timer.Builder setTimerStatus(int);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillisInElapsedRealtime(long);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStatus(int);
     method public androidx.appsearch.builtintypes.Timer.Builder setTtlMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setVibrate(boolean);
   }
diff --git a/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt b/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
index e867102c..e387d30 100644
--- a/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-builtin-types/api/public_plus_experimental_current.txt
@@ -12,16 +12,94 @@
 
 package androidx.appsearch.builtintypes {
 
-  @androidx.appsearch.annotation.Document public class Timer {
+  @androidx.appsearch.annotation.Document(name="builtin:Alarm") public class Alarm {
+    method public long getBlackoutEndTimeMillis();
+    method public long getBlackoutStartTimeMillis();
+    method public long getCreationTimestampMillis();
+    method public int[]? getDaysOfWeek();
+    method @IntRange(from=0, to=23) public int getHour();
+    method public String getId();
+    method @IntRange(from=0, to=59) public int getMinute();
+    method public String? getName();
+    method public String getNamespace();
+    method public androidx.appsearch.builtintypes.AlarmInstance? getNextInstance();
+    method public androidx.appsearch.builtintypes.AlarmInstance? getPreviousInstance();
+    method public String? getRingtone();
+    method public int getScore();
+    method public long getTtlMillis();
+    method public boolean isEnabled();
+    method public boolean isVibrate();
+  }
+
+  public static final class Alarm.Builder {
+    ctor public Alarm.Builder(String, String);
+    ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
+    method public androidx.appsearch.builtintypes.Alarm build();
+    method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutEndTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutStartTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setDaysOfWeek(@IntRange(from=java.util.Calendar.SUNDAY, to=java.util.Calendar.SATURDAY) int...);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setEnabled(boolean);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setHour(@IntRange(from=0, to=23) int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setMinute(@IntRange(from=0, to=59) int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setName(String?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setNextInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setPreviousInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setRingtone(String?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setScore(int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setTtlMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setVibrate(boolean);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:AlarmInstance") public class AlarmInstance {
+    method public long getCreationTimestampMillis();
+    method @IntRange(from=1, to=31) public int getDay();
+    method @IntRange(from=0, to=23) public int getHour();
+    method public String getId();
+    method @IntRange(from=0, to=59) public int getMinute();
+    method @IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) public int getMonth();
+    method public String getNamespace();
+    method public int getScore();
+    method public long getSnoozeDurationMillis();
+    method public int getStatus();
+    method public long getTtlMillis();
+    method public int getYear();
+    field public static final int STATUS_DISMISSED = 3; // 0x3
+    field public static final int STATUS_FIRING = 2; // 0x2
+    field public static final int STATUS_MISSED = 5; // 0x5
+    field public static final int STATUS_SCHEDULED = 1; // 0x1
+    field public static final int STATUS_SNOOZED = 4; // 0x4
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class AlarmInstance.Builder {
+    ctor public AlarmInstance.Builder(String, String);
+    ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
+    method public androidx.appsearch.builtintypes.AlarmInstance build();
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDay(@IntRange(from=1, to=31) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setHour(@IntRange(from=0, to=23) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMinute(@IntRange(from=0, to=59) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMonth(@IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setScore(int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setSnoozeDurationMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setStatus(int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setTtlMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setYear(int);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:Timer") public class Timer {
+    method public long getCreationTimestampMillis();
     method public long getDurationMillis();
-    method public long getExpireTimeMillis();
     method public String getId();
     method public String? getName();
     method public String getNamespace();
     method public long getRemainingTimeMillis();
     method public String? getRingtone();
     method public int getScore();
-    method public int getTimerStatus();
+    method public long getStartTimeMillis();
+    method public long getStartTimeMillisInElapsedRealtime();
+    method public int getStatus();
     method public long getTtlMillis();
     method public boolean isVibrate();
     field public static final int STATUS_EXPIRED = 3; // 0x3
@@ -34,14 +112,17 @@
 
   public static final class Timer.Builder {
     ctor public Timer.Builder(String, String);
+    ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
     method public androidx.appsearch.builtintypes.Timer build();
+    method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setDurationMillis(long);
-    method public androidx.appsearch.builtintypes.Timer.Builder setExpireTimeMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setName(String?);
     method public androidx.appsearch.builtintypes.Timer.Builder setRemainingTimeMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setRingtone(String?);
     method public androidx.appsearch.builtintypes.Timer.Builder setScore(int);
-    method public androidx.appsearch.builtintypes.Timer.Builder setTimerStatus(int);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillisInElapsedRealtime(long);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStatus(int);
     method public androidx.appsearch.builtintypes.Timer.Builder setTtlMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setVibrate(boolean);
   }
diff --git a/appsearch/appsearch-builtin-types/api/restricted_current.txt b/appsearch/appsearch-builtin-types/api/restricted_current.txt
index 616ae1e..b69955c 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_current.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_current.txt
@@ -14,16 +14,94 @@
 
 package androidx.appsearch.builtintypes {
 
-  @androidx.appsearch.annotation.Document public class Timer {
+  @androidx.appsearch.annotation.Document(name="builtin:Alarm") public class Alarm {
+    method public long getBlackoutEndTimeMillis();
+    method public long getBlackoutStartTimeMillis();
+    method public long getCreationTimestampMillis();
+    method public int[]? getDaysOfWeek();
+    method @IntRange(from=0, to=23) public int getHour();
+    method public String getId();
+    method @IntRange(from=0, to=59) public int getMinute();
+    method public String? getName();
+    method public String getNamespace();
+    method public androidx.appsearch.builtintypes.AlarmInstance? getNextInstance();
+    method public androidx.appsearch.builtintypes.AlarmInstance? getPreviousInstance();
+    method public String? getRingtone();
+    method public int getScore();
+    method public long getTtlMillis();
+    method public boolean isEnabled();
+    method public boolean isVibrate();
+  }
+
+  public static final class Alarm.Builder {
+    ctor public Alarm.Builder(String, String);
+    ctor public Alarm.Builder(androidx.appsearch.builtintypes.Alarm);
+    method public androidx.appsearch.builtintypes.Alarm build();
+    method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutEndTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setBlackoutStartTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setCreationTimestampMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setDaysOfWeek(@IntRange(from=java.util.Calendar.SUNDAY, to=java.util.Calendar.SATURDAY) int...);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setEnabled(boolean);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setHour(@IntRange(from=0, to=23) int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setMinute(@IntRange(from=0, to=59) int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setName(String?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setNextInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setPreviousInstance(androidx.appsearch.builtintypes.AlarmInstance?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setRingtone(String?);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setScore(int);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setTtlMillis(long);
+    method public androidx.appsearch.builtintypes.Alarm.Builder setVibrate(boolean);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:AlarmInstance") public class AlarmInstance {
+    method public long getCreationTimestampMillis();
+    method @IntRange(from=1, to=31) public int getDay();
+    method @IntRange(from=0, to=23) public int getHour();
+    method public String getId();
+    method @IntRange(from=0, to=59) public int getMinute();
+    method @IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) public int getMonth();
+    method public String getNamespace();
+    method public int getScore();
+    method public long getSnoozeDurationMillis();
+    method public int getStatus();
+    method public long getTtlMillis();
+    method public int getYear();
+    field public static final int STATUS_DISMISSED = 3; // 0x3
+    field public static final int STATUS_FIRING = 2; // 0x2
+    field public static final int STATUS_MISSED = 5; // 0x5
+    field public static final int STATUS_SCHEDULED = 1; // 0x1
+    field public static final int STATUS_SNOOZED = 4; // 0x4
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class AlarmInstance.Builder {
+    ctor public AlarmInstance.Builder(String, String);
+    ctor public AlarmInstance.Builder(androidx.appsearch.builtintypes.AlarmInstance);
+    method public androidx.appsearch.builtintypes.AlarmInstance build();
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setCreationTimestampMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setDay(@IntRange(from=1, to=31) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setHour(@IntRange(from=0, to=23) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMinute(@IntRange(from=0, to=59) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setMonth(@IntRange(from=java.util.Calendar.JANUARY, to=java.util.Calendar.DECEMBER) int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setScore(int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setSnoozeDurationMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setStatus(int);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setTtlMillis(long);
+    method public androidx.appsearch.builtintypes.AlarmInstance.Builder setYear(int);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:Timer") public class Timer {
+    method public long getCreationTimestampMillis();
     method public long getDurationMillis();
-    method public long getExpireTimeMillis();
     method public String getId();
     method public String? getName();
     method public String getNamespace();
     method public long getRemainingTimeMillis();
     method public String? getRingtone();
     method public int getScore();
-    method public int getTimerStatus();
+    method public long getStartTimeMillis();
+    method public long getStartTimeMillisInElapsedRealtime();
+    method public int getStatus();
     method public long getTtlMillis();
     method public boolean isVibrate();
     field public static final int STATUS_EXPIRED = 3; // 0x3
@@ -36,14 +114,17 @@
 
   public static final class Timer.Builder {
     ctor public Timer.Builder(String, String);
+    ctor public Timer.Builder(androidx.appsearch.builtintypes.Timer);
     method public androidx.appsearch.builtintypes.Timer build();
+    method public androidx.appsearch.builtintypes.Timer.Builder setCreationTimestampMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setDurationMillis(long);
-    method public androidx.appsearch.builtintypes.Timer.Builder setExpireTimeMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setName(String?);
     method public androidx.appsearch.builtintypes.Timer.Builder setRemainingTimeMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setRingtone(String?);
     method public androidx.appsearch.builtintypes.Timer.Builder setScore(int);
-    method public androidx.appsearch.builtintypes.Timer.Builder setTimerStatus(int);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillis(long);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStartTimeMillisInElapsedRealtime(long);
+    method public androidx.appsearch.builtintypes.Timer.Builder setStatus(int);
     method public androidx.appsearch.builtintypes.Timer.Builder setTtlMillis(long);
     method public androidx.appsearch.builtintypes.Timer.Builder setVibrate(boolean);
   }
diff --git a/appsearch/appsearch-builtin-types/build.gradle b/appsearch/appsearch-builtin-types/build.gradle
index 5dea7cc..5e7d9bd 100644
--- a/appsearch/appsearch-builtin-types/build.gradle
+++ b/appsearch/appsearch-builtin-types/build.gradle
@@ -35,6 +35,7 @@
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.multidex)
+    androidTestImplementation("junit:junit:4.13")
 }
 
 androidx {
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java
index 6c60cb6..5c8188f 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/app/ShortcutAdapterTest.java
@@ -106,15 +106,16 @@
 
     private static Timer timer(@NonNull final String id, @NonNull final String name) {
         return new Timer.Builder(ShortcutAdapter.DEFAULT_NAMESPACE, id)
-                .setTtlMillis(1000)
+                .setScore(1)
+                .setTtlMillis(6000)
+                .setCreationTimestampMillis(100)
                 .setName(name)
                 .setDurationMillis(1000)
-                .setTimerStatus(Timer.STATUS_STARTED)
                 .setRemainingTimeMillis(500)
-                .setExpireTimeMillis(2000)
                 .setRingtone("clock://ringtone/1")
-                .setScore(1)
+                .setStatus(Timer.STATUS_STARTED)
                 .setVibrate(true)
+                .setStartTimeMillis(750)
                 .build();
     }
 }
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmInstanceTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmInstanceTest.java
new file mode 100644
index 0000000..b37aae7e
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmInstanceTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.appsearch.builtintypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+
+import org.junit.Test;
+
+import java.util.Calendar;
+
+public class AlarmInstanceTest {
+    @Test
+    public void testBuilder() {
+        AlarmInstance alarmInstance = new AlarmInstance.Builder("namespace", "id")
+                .setScore(1)
+                .setTtlMillis(20000)
+                .setCreationTimestampMillis(100)
+                .setYear(2022)
+                .setMonth(Calendar.DECEMBER)
+                .setDay(1)
+                .setHour(7)
+                .setMinute(30)
+                .setStatus(AlarmInstance.STATUS_SCHEDULED)
+                .setSnoozeDurationMillis(10000)
+                .build();
+
+        assertThat(alarmInstance.getNamespace()).isEqualTo("namespace");
+        assertThat(alarmInstance.getId()).isEqualTo("id");
+        assertThat(alarmInstance.getScore()).isEqualTo(1);
+        assertThat(alarmInstance.getTtlMillis()).isEqualTo(20000);
+        assertThat(alarmInstance.getCreationTimestampMillis()).isEqualTo(100);
+        assertThat(alarmInstance.getYear()).isEqualTo(2022);
+        assertThat(alarmInstance.getMonth()).isEqualTo(Calendar.DECEMBER);
+        assertThat(alarmInstance.getDay()).isEqualTo(1);
+        assertThat(alarmInstance.getHour()).isEqualTo(7);
+        assertThat(alarmInstance.getMinute()).isEqualTo(30);
+        assertThat(alarmInstance.getStatus()).isEqualTo(1);
+        assertThat(alarmInstance.getSnoozeDurationMillis()).isEqualTo(10000);
+    }
+
+    @Test
+    public void testBuilderCopy_returnsAlarmInstanceWithAllFieldsCopied() {
+        AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "id")
+                .setScore(1)
+                .setTtlMillis(20000)
+                .setCreationTimestampMillis(100)
+                .setYear(2022)
+                .setMonth(Calendar.DECEMBER)
+                .setDay(1)
+                .setHour(7)
+                .setMinute(30)
+                .setStatus(AlarmInstance.STATUS_SCHEDULED)
+                .setSnoozeDurationMillis(10000)
+                .build();
+
+        AlarmInstance alarmInstance2 = new AlarmInstance.Builder(alarmInstance1).build();
+        assertThat(alarmInstance1.getNamespace()).isEqualTo(alarmInstance2.getNamespace());
+        assertThat(alarmInstance1.getId()).isEqualTo(alarmInstance2.getId());
+        assertThat(alarmInstance1.getScore()).isEqualTo(alarmInstance2.getScore());
+        assertThat(alarmInstance1.getTtlMillis()).isEqualTo(alarmInstance2.getTtlMillis());
+        assertThat(alarmInstance1.getCreationTimestampMillis())
+                .isEqualTo(alarmInstance2.getCreationTimestampMillis());
+        assertThat(alarmInstance1.getYear()).isEqualTo(alarmInstance2.getYear());
+        assertThat(alarmInstance1.getMonth()).isEqualTo(alarmInstance2.getMonth());
+        assertThat(alarmInstance1.getDay()).isEqualTo(alarmInstance2.getDay());
+        assertThat(alarmInstance1.getHour()).isEqualTo(alarmInstance2.getHour());
+        assertThat(alarmInstance1.getMinute()).isEqualTo(alarmInstance2.getMinute());
+        assertThat(alarmInstance1.getStatus()).isEqualTo(alarmInstance2.getStatus());
+        assertThat(alarmInstance1.getSnoozeDurationMillis())
+                .isEqualTo(alarmInstance2.getSnoozeDurationMillis());
+    }
+
+    @Test
+    public void testToGenericDocument() throws Exception {
+        AlarmInstance alarmInstance = new AlarmInstance.Builder("namespace", "id")
+                .setScore(1)
+                .setTtlMillis(20000)
+                .setCreationTimestampMillis(100)
+                .setYear(2022)
+                .setMonth(Calendar.DECEMBER)
+                .setDay(1)
+                .setHour(7)
+                .setMinute(30)
+                .setStatus(AlarmInstance.STATUS_SCHEDULED)
+                .setSnoozeDurationMillis(10000)
+                .build();
+
+        GenericDocument genericDocument = GenericDocument.fromDocumentClass(alarmInstance);
+        assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:AlarmInstance");
+        assertThat(genericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(genericDocument.getId()).isEqualTo("id");
+        assertThat(genericDocument.getScore()).isEqualTo(1);
+        assertThat(genericDocument.getTtlMillis()).isEqualTo(20000);
+        assertThat(genericDocument.getCreationTimestampMillis()).isEqualTo(100);
+        assertThat(genericDocument.getPropertyLong("year")).isEqualTo(2022);
+        assertThat(genericDocument.getPropertyLong("month")).isEqualTo(Calendar.DECEMBER);
+        assertThat(genericDocument.getPropertyLong("day")).isEqualTo(1);
+        assertThat(genericDocument.getPropertyLong("hour")).isEqualTo(7);
+        assertThat(genericDocument.getPropertyLong("minute")).isEqualTo(30);
+        assertThat(genericDocument.getPropertyLong("status")).isEqualTo(1);
+        assertThat(genericDocument.getPropertyLong("snoozeDurationMillis")).isEqualTo(10000);
+    }
+}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmTest.java
new file mode 100644
index 0000000..b953f60
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/AlarmTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.appsearch.builtintypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+
+import org.junit.Test;
+
+import java.util.Calendar;
+
+public class AlarmTest {
+    @Test
+    public void testBuilder() {
+        AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "instanceId1")
+                .build();
+        AlarmInstance alarmInstance2 = new AlarmInstance.Builder("namespace", "instanceId2")
+                .build();
+        Alarm alarm = new Alarm.Builder("namespace", "id")
+                .setScore(1)
+                .setTtlMillis(20000)
+                .setCreationTimestampMillis(100)
+                .setName("my alarm")
+                .setEnabled(true)
+                .setDaysOfWeek(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+                        Calendar.THURSDAY, Calendar.FRIDAY)
+                .setHour(12)
+                .setMinute(0)
+                .setBlackoutStartTimeMillis(1000)
+                .setBlackoutEndTimeMillis(2000)
+                .setRingtone("clock://ringtone/1")
+                .setVibrate(true)
+                .setPreviousInstance(alarmInstance1)
+                .setNextInstance(alarmInstance2)
+                .build();
+
+        assertThat(alarm.getNamespace()).isEqualTo("namespace");
+        assertThat(alarm.getId()).isEqualTo("id");
+        assertThat(alarm.getScore()).isEqualTo(1);
+        assertThat(alarm.getTtlMillis()).isEqualTo(20000);
+        assertThat(alarm.getCreationTimestampMillis()).isEqualTo(100);
+        assertThat(alarm.getName()).isEqualTo("my alarm");
+        assertThat(alarm.isEnabled()).isTrue();
+        assertThat(alarm.getDaysOfWeek()).asList().containsExactly(Calendar.MONDAY,
+                Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY);
+        assertThat(alarm.getHour()).isEqualTo(12);
+        assertThat(alarm.getMinute()).isEqualTo(0);
+        assertThat(alarm.getBlackoutStartTimeMillis()).isEqualTo(1000);
+        assertThat(alarm.getBlackoutEndTimeMillis()).isEqualTo(2000);
+        assertThat(alarm.getRingtone()).isEqualTo("clock://ringtone/1");
+        assertThat(alarm.isVibrate()).isTrue();
+        assertThat(alarm.getPreviousInstance()).isEqualTo(alarmInstance1);
+        assertThat(alarm.getNextInstance()).isEqualTo(alarmInstance2);
+    }
+
+    @Test
+    public void testBuilderCopy_returnsAlarmWithAllFieldsCopied() {
+        AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "instanceId1")
+                .build();
+        AlarmInstance alarmInstance2 = new AlarmInstance.Builder("namespace", "instanceId2")
+                .build();
+        Alarm alarm1 = new Alarm.Builder("namespace", "id")
+                .setScore(1)
+                .setTtlMillis(20000)
+                .setCreationTimestampMillis(100)
+                .setName("my alarm")
+                .setEnabled(true)
+                .setDaysOfWeek(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+                        Calendar.THURSDAY, Calendar.FRIDAY)
+                .setHour(12)
+                .setMinute(0)
+                .setBlackoutStartTimeMillis(1000)
+                .setBlackoutEndTimeMillis(2000)
+                .setRingtone("clock://ringtone/1")
+                .setVibrate(true)
+                .setPreviousInstance(alarmInstance1)
+                .setNextInstance(alarmInstance2)
+                .build();
+
+        Alarm alarm2 = new Alarm.Builder(alarm1).build();
+        assertThat(alarm1.getNamespace()).isEqualTo(alarm2.getNamespace());
+        assertThat(alarm1.getId()).isEqualTo(alarm2.getId());
+        assertThat(alarm1.getScore()).isEqualTo(alarm2.getScore());
+        assertThat(alarm1.getTtlMillis()).isEqualTo(alarm2.getTtlMillis());
+        assertThat(alarm1.getCreationTimestampMillis())
+                .isEqualTo(alarm2.getCreationTimestampMillis());
+        assertThat(alarm1.getName()).isEqualTo(alarm2.getName());
+        assertThat(alarm1.isEnabled()).isEqualTo(alarm2.isEnabled());
+        assertThat(alarm1.getDaysOfWeek()).isEqualTo(alarm2.getDaysOfWeek());
+        assertThat(alarm1.getHour()).isEqualTo(alarm2.getHour());
+        assertThat(alarm1.getMinute()).isEqualTo(alarm2.getMinute());
+        assertThat(alarm1.getBlackoutStartTimeMillis())
+                .isEqualTo(alarm2.getBlackoutStartTimeMillis());
+        assertThat(alarm1.getBlackoutEndTimeMillis()).isEqualTo(alarm2.getBlackoutEndTimeMillis());
+        assertThat(alarm1.getRingtone()).isEqualTo(alarm2.getRingtone());
+        assertThat(alarm1.isVibrate()).isEqualTo(alarm2.isVibrate());
+        assertThat(alarm1.getPreviousInstance()).isEqualTo(alarm2.getPreviousInstance());
+        assertThat(alarm1.getNextInstance()).isEqualTo(alarm2.getNextInstance());
+    }
+
+    @Test
+    public void testToGenericDocument() throws Exception {
+        AlarmInstance alarmInstance1 = new AlarmInstance.Builder("namespace", "instanceId1")
+                .setCreationTimestampMillis(100)
+                .build();
+        AlarmInstance alarmInstance2 = new AlarmInstance.Builder("namespace", "instanceId2")
+                .setCreationTimestampMillis(100)
+                .build();
+        Alarm alarm = new Alarm.Builder("namespace", "id")
+                .setScore(1)
+                .setTtlMillis(20000)
+                .setCreationTimestampMillis(100)
+                .setName("my alarm")
+                .setEnabled(true)
+                .setDaysOfWeek(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+                        Calendar.THURSDAY, Calendar.FRIDAY)
+                .setHour(12)
+                .setMinute(0)
+                .setBlackoutStartTimeMillis(1000)
+                .setBlackoutEndTimeMillis(2000)
+                .setRingtone("clock://ringtone/1")
+                .setVibrate(true)
+                .setPreviousInstance(alarmInstance1)
+                .setNextInstance(alarmInstance2)
+                .build();
+
+        GenericDocument genericDocument = GenericDocument.fromDocumentClass(alarm);
+        assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:Alarm");
+        assertThat(genericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(genericDocument.getId()).isEqualTo("id");
+        assertThat(genericDocument.getScore()).isEqualTo(1);
+        assertThat(genericDocument.getTtlMillis()).isEqualTo(20000);
+        assertThat(genericDocument.getCreationTimestampMillis()).isEqualTo(100);
+        assertThat(genericDocument.getPropertyString("name")).isEqualTo("my alarm");
+        assertThat(genericDocument.getPropertyBoolean("enabled")).isTrue();
+        assertThat(genericDocument.getPropertyLongArray("daysOfWeek")).asList()
+                .containsExactly(2L, 3L, 4L, 5L, 6L);
+        assertThat(genericDocument.getPropertyLong("hour")).isEqualTo(12);
+        assertThat(genericDocument.getPropertyLong("minute")).isEqualTo(0);
+        assertThat(genericDocument.getPropertyLong("blackoutStartTimeMillis")).isEqualTo(1000);
+        assertThat(genericDocument.getPropertyLong("blackoutEndTimeMillis")).isEqualTo(2000);
+        assertThat(genericDocument.getPropertyString("ringtone")).isEqualTo("clock://ringtone/1");
+        assertThat(genericDocument.getPropertyBoolean("vibrate")).isTrue();
+        assertThat(genericDocument.getPropertyDocument("previousInstance"))
+                .isEqualTo(GenericDocument.fromDocumentClass(alarmInstance1));
+        assertThat(genericDocument.getPropertyDocument("nextInstance"))
+                .isEqualTo(GenericDocument.fromDocumentClass(alarmInstance2));
+    }
+
+    @Test
+    public void testBuilder_invalidByDay_throwsError() {
+        assertThrows(IllegalArgumentException.class, () -> new Alarm.Builder("namespace", "id")
+                .setDaysOfWeek(Calendar.MONDAY, Calendar.SUNDAY, 8)
+                .build());
+    }
+
+    @Test
+    public void testBuilder_invalidHour_throwsError() {
+        assertThrows(IllegalArgumentException.class, () -> new Alarm.Builder("namespace", "id")
+                .setHour(24)
+                .build());
+    }
+
+    @Test
+    public void testBuilder_invalidMinute_throwsError() {
+        assertThrows(IllegalArgumentException.class, () -> new Alarm.Builder("namespace", "id")
+                .setMinute(60)
+                .build());
+    }
+
+    @Test
+    public void testAlarmWithNullDaysOfWeek_shouldReturnNullDaysOfWeek() throws Exception {
+        Alarm alarm = new Alarm.Builder("namespace", "id")
+                .build();
+        GenericDocument alarmGenericDocument = GenericDocument.fromDocumentClass(alarm);
+
+        assertThat(alarm.getDaysOfWeek()).isNull();
+        assertThat(alarmGenericDocument.getPropertyLongArray("daysOfWeek")).isNull();
+    }
+}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java
index e4c8b92a..ca78c58 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/TimerTest.java
@@ -26,60 +26,99 @@
     @Test
     public void testBuilder() {
         Timer timer = new Timer.Builder("namespace", "id1")
+                .setScore(1)
                 .setTtlMillis(6000)
+                .setCreationTimestampMillis(100)
                 .setName("my timer")
                 .setDurationMillis(1000)
-                .setTimerStatus(Timer.STATUS_STARTED)
                 .setRemainingTimeMillis(500)
-                .setExpireTimeMillis(2000)
                 .setRingtone("clock://ringtone/1")
-                .setScore(1)
+                .setStatus(Timer.STATUS_STARTED)
                 .setVibrate(true)
+                .setStartTimeMillis(750)
+                .setStartTimeMillisInElapsedRealtime(700L)
                 .build();
 
         assertThat(timer.getNamespace()).isEqualTo("namespace");
         assertThat(timer.getId()).isEqualTo("id1");
         assertThat(timer.getTtlMillis()).isEqualTo(6000);
+        assertThat(timer.getScore()).isEqualTo(1);
+        assertThat(timer.getCreationTimestampMillis()).isEqualTo(100);
         assertThat(timer.getName()).isEqualTo("my timer");
         assertThat(timer.getDurationMillis()).isEqualTo(1000);
-        assertThat(timer.getTimerStatus()).isEqualTo(1);
         assertThat(timer.getRemainingTimeMillis()).isEqualTo(500);
-        assertThat(timer.getExpireTimeMillis()).isEqualTo(2000);
         assertThat(timer.getRingtone()).isEqualTo("clock://ringtone/1");
-        assertThat(timer.getScore()).isEqualTo(1);
+        assertThat(timer.getStatus()).isEqualTo(1);
         assertThat(timer.isVibrate()).isEqualTo(true);
+        assertThat(timer.getStartTimeMillis()).isEqualTo(750);
+        assertThat(timer.getStartTimeMillisInElapsedRealtime()).isEqualTo(700);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsCopied() {
+        Timer timer1 = new Timer.Builder("namespace", "id1")
+                .setScore(1)
+                .setTtlMillis(6000)
+                .setCreationTimestampMillis(100)
+                .setName("my timer")
+                .setDurationMillis(1000)
+                .setRemainingTimeMillis(500)
+                .setRingtone("clock://ringtone/1")
+                .setStatus(Timer.STATUS_STARTED)
+                .setVibrate(true)
+                .setStartTimeMillis(750)
+                .setStartTimeMillisInElapsedRealtime(700L)
+                .build();
+        Timer timer2 = new Timer.Builder(timer1).build();
+
+        assertThat(timer1.getNamespace()).isEqualTo(timer2.getNamespace());
+        assertThat(timer1.getId()).isEqualTo(timer2.getId());
+        assertThat(timer1.getTtlMillis()).isEqualTo(timer2.getTtlMillis());
+        assertThat(timer1.getScore()).isEqualTo(timer2.getScore());
+        assertThat(timer1.getCreationTimestampMillis())
+                .isEqualTo(timer2.getCreationTimestampMillis());
+        assertThat(timer1.getName()).isEqualTo(timer2.getName());
+        assertThat(timer1.getDurationMillis()).isEqualTo(timer2.getDurationMillis());
+        assertThat(timer1.getRemainingTimeMillis()).isEqualTo(timer2.getRemainingTimeMillis());
+        assertThat(timer1.getRingtone()).isEqualTo(timer2.getRingtone());
+        assertThat(timer1.getStatus()).isEqualTo(timer2.getStatus());
+        assertThat(timer1.isVibrate()).isEqualTo(timer2.isVibrate());
+        assertThat(timer1.getStartTimeMillis()).isEqualTo(timer2.getStartTimeMillis());
+        assertThat(timer1.getStartTimeMillisInElapsedRealtime())
+                .isEqualTo(timer2.getStartTimeMillisInElapsedRealtime());
     }
 
     @Test
     public void testToGenericDocument() throws Exception {
         Timer timer = new Timer.Builder("namespace", "id1")
+                .setScore(1)
                 .setTtlMillis(6000)
+                .setCreationTimestampMillis(100)
                 .setName("my timer")
                 .setDurationMillis(1000)
-                .setTimerStatus(Timer.STATUS_STARTED)
                 .setRemainingTimeMillis(500)
-                .setExpireTimeMillis(2000)
                 .setRingtone("clock://ringtone/1")
-                .setScore(1)
+                .setStatus(Timer.STATUS_STARTED)
                 .setVibrate(true)
+                .setStartTimeMillis(750)
+                .setStartTimeMillisInElapsedRealtime(700L)
                 .build();
 
         GenericDocument genericDocument = GenericDocument.fromDocumentClass(timer);
+        assertThat(genericDocument.getSchemaType()).isEqualTo("builtin:Timer");
         assertThat(genericDocument.getNamespace()).isEqualTo("namespace");
         assertThat(genericDocument.getId()).isEqualTo("id1");
-        assertThat(genericDocument.getScore()).isEqualTo(1);
         assertThat(genericDocument.getTtlMillis()).isEqualTo(6000);
-        assertThat(genericDocument.getSchemaType()).isEqualTo("Timer");
-        assertThat(genericDocument.getPropertyString("name"))
-                .isEqualTo("my timer");
+        assertThat(genericDocument.getScore()).isEqualTo(1);
+        assertThat(genericDocument.getCreationTimestampMillis()).isEqualTo(100);
+        assertThat(genericDocument.getPropertyString("name")).isEqualTo("my timer");
         assertThat(genericDocument.getPropertyLong("durationMillis")).isEqualTo(1000);
-        assertThat(genericDocument.getPropertyLong("timerStatus"))
-                .isEqualTo(1);
         assertThat(genericDocument.getPropertyLong("remainingTimeMillis")).isEqualTo(500);
-        assertThat(genericDocument.getPropertyLong("expireTimeMillis"))
-                .isEqualTo(2000);
-        assertThat(genericDocument.getPropertyString("ringtone"))
-                .isEqualTo("clock://ringtone/1");
-        assertThat(genericDocument.getPropertyBoolean("vibrate")).isEqualTo(true);
+        assertThat(genericDocument.getPropertyString("ringtone")).isEqualTo("clock://ringtone/1");
+        assertThat(genericDocument.getPropertyLong("status")).isEqualTo(1);
+        assertThat(genericDocument.getPropertyBoolean("vibrate")).isTrue();
+        assertThat(genericDocument.getPropertyLong("startTimeMillis")).isEqualTo(750);
+        assertThat(genericDocument.getPropertyLong("startTimeMillisInElapsedRealtime"))
+                .isEqualTo(700);
     }
 }
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
new file mode 100644
index 0000000..69d384b
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Alarm.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.appsearch.builtintypes;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.core.util.Preconditions;
+
+import java.util.Calendar;
+
+/**
+ * AppSearch document representing an Alarm entity.
+ */
+@Document(name = "builtin:Alarm")
+public class Alarm {
+    @Document.Namespace
+    private final String mNamespace;
+
+    @Document.Id
+    private final String mId;
+
+    @Document.Score
+    private final int mScore;
+
+    @Document.CreationTimestampMillis
+    private final long mCreationTimestampMillis;
+
+    @Document.TtlMillis
+    private final long mTtlMillis;
+
+    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    private final String mName;
+
+    @Document.BooleanProperty
+    private final boolean mEnabled;
+
+    @Document.LongProperty
+    private final int[] mDaysOfWeek;
+
+    @Document.LongProperty
+    private final int mHour;
+
+    @Document.LongProperty
+    private final int mMinute;
+
+    @Document.LongProperty
+    private final long mBlackoutStartTimeMillis;
+
+    @Document.LongProperty
+    private final long mBlackoutEndTimeMillis;
+
+    @Document.StringProperty
+    private final String mRingtone;
+
+    @Document.BooleanProperty
+    private final boolean mVibrate;
+
+    @Document.DocumentProperty
+    private final AlarmInstance mPreviousInstance;
+
+    @Document.DocumentProperty
+    private final AlarmInstance mNextInstance;
+
+    Alarm(String namespace, String id, int score, long creationTimestampMillis, long ttlMillis,
+            String name, boolean enabled, int[] daysOfWeek, int hour, int minute,
+            long blackoutStartTimeMillis, long blackoutEndTimeMillis, String ringtone,
+            boolean vibrate, AlarmInstance previousInstance, AlarmInstance nextInstance) {
+        mNamespace = namespace;
+        mId = id;
+        mScore = score;
+        mCreationTimestampMillis = creationTimestampMillis;
+        mTtlMillis = ttlMillis;
+        mName = name;
+        mEnabled = enabled;
+        mDaysOfWeek = daysOfWeek;
+        mHour = hour;
+        mMinute = minute;
+        mBlackoutStartTimeMillis = blackoutStartTimeMillis;
+        mBlackoutEndTimeMillis = blackoutEndTimeMillis;
+        mRingtone = ringtone;
+        mVibrate = vibrate;
+        mPreviousInstance = previousInstance;
+        mNextInstance = nextInstance;
+    }
+
+    /** Returns the namespace of the {@link Alarm}. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the unique identifier of the {@link Alarm}. */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the user-provided opaque document score of the {@link Alarm}, which can be
+     * used for ranking using
+     * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}.
+     */
+    public int getScore() {
+        return mScore;
+    }
+
+    /**
+     * Returns the creation timestamp for the {@link Alarm} document, in milliseconds using the
+     * {@link System#currentTimeMillis()} time base.
+     */
+    public long getCreationTimestampMillis() {
+        return mCreationTimestampMillis;
+    }
+
+    /**
+     * Returns the time-to-live (TTL) for the {@link Alarm} document in milliseconds using the
+     * {@link System#currentTimeMillis()} time base.
+     *
+     * <p>The {@link Alarm} document will be automatically deleted when the TTL expires.
+     */
+    public long getTtlMillis() {
+        return mTtlMillis;
+    }
+
+    /** Returns the name associated with the {@link Alarm}. */
+    @Nullable
+    public String getName() {
+        return mName;
+    }
+
+    /** Returns whether or not the {@link Alarm} is active. */
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    /**
+     * Returns the scheduled days for repeating the {@link Alarm}.
+     *
+     * <p>Days of the week can be {@link java.util.Calendar#MONDAY},
+     * {@link java.util.Calendar#TUESDAY}, {@link java.util.Calendar#WEDNESDAY},
+     * {@link java.util.Calendar#THURSDAY}, {@link java.util.Calendar#FRIDAY},
+     * {@link java.util.Calendar#SATURDAY}, or {@link java.util.Calendar#SUNDAY}.
+     *
+     * <p>If null, or if the list is empty, then the {@link Alarm} does not repeat.
+     */
+    @Nullable
+    public int[] getDaysOfWeek() {
+        return mDaysOfWeek;
+    }
+
+    /**
+     * Returns the hour the {@link Alarm} will fire.
+     *
+     * <p>Hours are specified by integers from 0 to 23.
+     */
+    @IntRange(from = 0, to = 23)
+    public int getHour() {
+        return mHour;
+    }
+
+    /**
+     * Returns the minute the {@link Alarm} will fire.
+     *
+     * <p>Minutes are specified by integers from 0 to 59.
+     */
+    @IntRange(from = 0, to = 59)
+    public int getMinute() {
+        return mMinute;
+    }
+
+    /**
+     * Returns the start time for the {@link Alarm} blackout period in milliseconds using the
+     * {@link System#currentTimeMillis()} time base.
+     *
+     * <p>A blackout period means the {@link Alarm} will not fire during this period.
+     *
+     * <p>The value {@code 0} indicates that the blackout period has no start time.
+     *
+     * <p>If both blackoutStartTime and blackoutEndTime are {@code 0}, then the blackout period
+     * is not defined for this {@link Alarm}.
+     */
+    public long getBlackoutStartTimeMillis() {
+        return mBlackoutStartTimeMillis;
+    }
+
+    /**
+     * Returns the end time for the {@link Alarm} blackout period in milliseconds using the
+     * {@link System#currentTimeMillis()} time base.
+     *
+     * <p>A blackout period means the {@link Alarm} will not fire during this period.
+     *
+     * <p>The value {@code 0} indicates that the blackout period has no end time.
+     *
+     * <p>If both blackoutStartTime and blackoutEndTime are {@code 0}, then the blackout period
+     * is not defined for this {@link Alarm}.
+     */
+    public long getBlackoutEndTimeMillis() {
+        return mBlackoutEndTimeMillis;
+    }
+
+    /**
+     * Returns the ringtone of the {@link Alarm} as a content URI to be played, or
+     * {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
+     */
+    @Nullable
+    public String getRingtone() {
+        return mRingtone;
+    }
+
+    /** Returns whether or not to activate the device vibrator when the {@link Alarm} fires. */
+    public boolean isVibrate() {
+        return mVibrate;
+    }
+
+    /**
+     * Returns the previous {@link AlarmInstance} associated with the {@link Alarm}.
+     *
+     * <p>The previous {@link AlarmInstance} is most recent past instance that was fired. If the
+     * {@link Alarm} has no past instances, then null will be returned.
+     *
+     * <p>See {@link AlarmInstance}.
+     */
+    @Nullable
+    public AlarmInstance getPreviousInstance() {
+        return mPreviousInstance;
+    }
+
+    /**
+     * Returns the next {@link AlarmInstance} associated with the {@link Alarm}.
+     *
+     * <p>The next {@link AlarmInstance} is the immediate future instance that is scheduled to fire.
+     * If the {@link Alarm} has no future instances, then null will be returned.
+     *
+     * <p>See {@link AlarmInstance}.
+     */
+    @Nullable
+    public AlarmInstance getNextInstance() {
+        return mNextInstance;
+    }
+
+    /** Builder for {@link Alarm}. */
+    public static final class Builder {
+        private final String mNamespace;
+        private final String mId;
+        private int mScore;
+        private long mCreationTimestampMillis;
+        private long mTtlMillis;
+        private String mName;
+        private boolean mEnabled;
+        private int[] mDaysOfWeek;
+        private int mHour;
+        private int mMinute;
+        private long mBlackoutStartTimeMillis;
+        private long mBlackoutEndTimeMillis;
+        private String mRingtone;
+        private boolean mVibrate;
+        private AlarmInstance mPreviousInstance;
+        private AlarmInstance mNextInstance;
+
+        /**
+         * Constructor for {@link Alarm.Builder}.
+         *
+         * @param namespace Namespace for the {@link Alarm} Document. See
+         * {@link Document.Namespace}.
+         * @param id Unique identifier for the {@link Alarm} Document. See {@link Document.Id}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mId = Preconditions.checkNotNull(id);
+
+            // Default for unset creationTimestampMillis. AppSearch will internally convert this
+            // to current time when creating the GenericDocument.
+            mCreationTimestampMillis = -1;
+        }
+
+        /**
+         * Constructor for {@link Alarm.Builder} with all the existing values of an {@link Alarm}.
+         */
+        public Builder(@NonNull Alarm alarm) {
+            this(alarm.getNamespace(), alarm.getId());
+            mScore = alarm.getScore();
+            mCreationTimestampMillis = alarm.getCreationTimestampMillis();
+            mTtlMillis = alarm.getTtlMillis();
+            mName = alarm.getName();
+            mEnabled = alarm.isEnabled();
+            mDaysOfWeek = alarm.getDaysOfWeek();
+            mHour = alarm.getHour();
+            mMinute = alarm.getMinute();
+            mBlackoutStartTimeMillis = alarm.getBlackoutStartTimeMillis();
+            mBlackoutEndTimeMillis = alarm.getBlackoutEndTimeMillis();
+            mRingtone = alarm.getRingtone();
+            mVibrate = alarm.isVibrate();
+            mPreviousInstance = alarm.getPreviousInstance();
+            mNextInstance = alarm.getNextInstance();
+        }
+
+        /**
+         * Sets the opaque document score of the {@link Alarm}, which can be used for
+         * ranking using
+         * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}
+         *
+         * <p>See {@link Document.Score}
+         */
+        @NonNull
+        public Alarm.Builder setScore(int score) {
+            mScore = score;
+            return this;
+        }
+
+        /**
+         * Sets the Creation Timestamp of the {@link Alarm} document, in milliseconds using the
+         * {@link System#currentTimeMillis()} time base.
+         *
+         * <p>If not set, then the current timestamp will be used.
+         *
+         * <p>See {@link Document.CreationTimestampMillis}
+         */
+        @NonNull
+        public Alarm.Builder setCreationTimestampMillis(long creationTimestampMillis) {
+            mCreationTimestampMillis = creationTimestampMillis;
+            return this;
+        }
+
+        /**
+         * Sets the time-to-live (TTL) for the {@link Alarm} document in milliseconds using the
+         * {@link System#currentTimeMillis()} time base.
+         *
+         * <p>The {@link Alarm} document will be automatically deleted when the TTL expires.
+         *
+         * <p>If set to 0, then the document will never expire.
+         *
+         * <p>See {@link Document.TtlMillis}
+         */
+        @NonNull
+        public Alarm.Builder setTtlMillis(long ttlMillis) {
+            mTtlMillis = ttlMillis;
+            return this;
+        }
+
+        /** Sets the name of the {@link Alarm}. */
+        @NonNull
+        public Alarm.Builder setName(@Nullable String name) {
+            mName = name;
+            return this;
+        }
+
+        /** Sets whether or not the {@link Alarm} is active. */
+        @NonNull
+        public Builder setEnabled(boolean enabled) {
+            mEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets the scheduled days for repeating the {@link Alarm}.
+         *
+         * <p>Days of the week can be {@link java.util.Calendar#MONDAY},
+         * {@link java.util.Calendar#TUESDAY}, {@link java.util.Calendar#WEDNESDAY},
+         * {@link java.util.Calendar#THURSDAY}, {@link java.util.Calendar#FRIDAY},
+         * {@link java.util.Calendar#SATURDAY}, or {@link java.util.Calendar#SUNDAY}.
+         *
+         * <p>If not set, or if the list is empty, then the {@link Alarm} does not repeat.
+         */
+        @NonNull
+        public Builder setDaysOfWeek(
+                @Nullable
+                @IntRange(from = Calendar.SUNDAY, to = Calendar.SATURDAY) int... daysOfWeek) {
+            if (daysOfWeek != null) {
+                for (int day : daysOfWeek) {
+                    Preconditions.checkArgumentInRange(day, Calendar.SUNDAY, Calendar.SATURDAY,
+                            "daysOfWeek");
+                }
+            }
+            mDaysOfWeek = daysOfWeek;
+            return this;
+        }
+
+        /**
+         * Sets the hour the {@link Alarm} will fire.
+         *
+         * <p>Hours are specified by integers from 0 to 23.
+         */
+        @NonNull
+        public Builder setHour(@IntRange(from = 0, to = 23) int hour) {
+            mHour = Preconditions.checkArgumentInRange(hour, 0, 23, "hour");
+            return this;
+        }
+
+        /**
+         * Sets the minute the {@link Alarm} will fire.
+         *
+         * <p>Minutes are specified by integers from 0 to 59.
+         */
+        @NonNull
+        public Builder setMinute(@IntRange(from = 0, to = 59) int minute) {
+            mMinute = Preconditions.checkArgumentInRange(minute, 0, 59, "minute");
+            return this;
+        }
+
+        /**
+         * Sets the start time for the {@link Alarm} blackout period in milliseconds using the
+         * {@link System#currentTimeMillis()} time base.
+         *
+         * <p>A blackout period means the {@link Alarm} will not fire during this period.
+         *
+         * <p>If not set, or set to 0, then the blackout period has no start time.
+         *
+         * <p>If neither blackoutStartTime nor blackoutEndTime are set, or if they are both set
+         * to 0, then the {@link Alarm} has no blackout period.
+         */
+        @NonNull
+        public Builder setBlackoutStartTimeMillis(long blackoutStartTimeMillis) {
+            mBlackoutStartTimeMillis = blackoutStartTimeMillis;
+            return this;
+        }
+
+        /**
+         * Sets the end time for the {@link Alarm} blackout period in milliseconds using the
+         * {@link System#currentTimeMillis()} time base.
+         *
+         * <p>A blackout period means the {@link Alarm} will not fire during this period.
+         *
+         * <p>If not set, or set to 0, then the blackout period has no end time.
+         *
+         * <p>If neither blackoutStartTime nor blackoutEndTime are set, or if they are both set
+         * to 0, then the {@link Alarm} has no blackout period.
+         */
+        @NonNull
+        public Builder setBlackoutEndTimeMillis(long blackoutEndTimeMillis) {
+            mBlackoutEndTimeMillis = blackoutEndTimeMillis;
+            return this;
+        }
+
+        /**
+         * Sets the content URI for the ringtone to be played, or
+         * {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
+         */
+        @NonNull
+        public Builder setRingtone(@Nullable String ringtone) {
+            mRingtone = ringtone;
+            return this;
+        }
+
+        /** Sets whether or not to activate the device vibrator when the {@link Alarm} fires. */
+        @NonNull
+        public Builder setVibrate(boolean vibrate) {
+            mVibrate = vibrate;
+            return this;
+        }
+
+        /**
+         * Sets the previous {@link AlarmInstance} associated with the {@link Alarm}.
+         *
+         * <p>The previous {@link AlarmInstance} is most recent past instance that was fired. If
+         * not set, then the {@link Alarm} has no past instances.
+         *
+         * <p>See {@link AlarmInstance}.
+         */
+        @NonNull
+        public Builder setPreviousInstance(@Nullable AlarmInstance previousInstance) {
+            mPreviousInstance = previousInstance;
+            return this;
+        }
+
+        /**
+         * Sets the next {@link AlarmInstance} associated with the {@link Alarm}.
+         *
+         * <p>The next {@link AlarmInstance} is the immediate future instance that is scheduled
+         * to fire. If not set, then the {@link Alarm} has no future instances.
+         *
+         * <p>See {@link AlarmInstance}.
+         */
+        @NonNull
+        public Builder setNextInstance(@Nullable AlarmInstance nextInstance) {
+            mNextInstance = nextInstance;
+            return this;
+        }
+
+        /** Builds the {@link Alarm}. */
+        @NonNull
+        public Alarm build() {
+            Preconditions.checkNotNull(mId);
+            Preconditions.checkNotNull(mNamespace);
+
+            return new Alarm(mNamespace, mId, mScore, mCreationTimestampMillis, mTtlMillis, mName,
+                    mEnabled, mDaysOfWeek, mHour, mMinute, mBlackoutStartTimeMillis,
+                    mBlackoutEndTimeMillis, mRingtone, mVibrate, mPreviousInstance, mNextInstance);
+        }
+    }
+}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
new file mode 100644
index 0000000..bf20151
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/AlarmInstance.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.appsearch.builtintypes;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+
+/**
+ * AppSearch document representing an AlarmInstance entity.
+ *
+ * <p>An {@link AlarmInstance} must be associated with an {@link Alarm}. It represents a
+ * particular point in time for that Alarm. For example, if an Alarm is set to
+ * repeat every Monday, then each AlarmInstance for it will be the exact Mondays that the Alarm
+ * did trigger.
+ *
+ * <p>Year, month, day, hour, and minute are used over timestamp to ensure the
+ * {@link AlarmInstance} remains unchanged across timezones. E.g. An AlarmInstance set to fire at
+ * 7am GMT should also fire at 7am when the timezone is changed to PST.
+ */
+@Document(name = "builtin:AlarmInstance")
+public class AlarmInstance {
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({STATUS_UNKNOWN, STATUS_SCHEDULED, STATUS_FIRING, STATUS_DISMISSED, STATUS_SNOOZED,
+            STATUS_MISSED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Status {}
+
+    /** The {@link AlarmInstance} is in an unknown error state. */
+    public static final int STATUS_UNKNOWN = 0;
+    /** The {@link AlarmInstance} is scheduled to fire at some point in the future. */
+    public static final int STATUS_SCHEDULED = 1;
+    /** The {@link AlarmInstance} is firing. */
+    public static final int STATUS_FIRING = 2;
+    /** The {@link AlarmInstance} has been dismissed. */
+    public static final int STATUS_DISMISSED = 3;
+    /** The {@link AlarmInstance} has been snoozed. */
+    public static final int STATUS_SNOOZED = 4;
+    /** The {@link AlarmInstance} has been missed. */
+    public static final int STATUS_MISSED = 5;
+
+    @Document.Namespace
+    private final String mNamespace;
+
+    @Document.Id
+    private final String mId;
+
+    @Document.Score
+    private final int mScore;
+
+    @Document.CreationTimestampMillis
+    private final long mCreationTimestampMillis;
+
+    @Document.TtlMillis
+    private final long mTtlMillis;
+
+    @Document.LongProperty
+    private final int mYear;
+
+    @Document.LongProperty
+    private final int mMonth;
+
+    @Document.LongProperty
+    private final int mDay;
+
+    @Document.LongProperty
+    private final int mHour;
+
+    @Document.LongProperty
+    private final int mMinute;
+
+    @Document.LongProperty
+    private final int mStatus;
+
+    @Document.LongProperty
+    private final long mSnoozeDurationMillis;
+
+    AlarmInstance(String namespace, String id, int score, long creationTimestampMillis,
+            long ttlMillis, int year, int month, int day, int hour, int minute, int status,
+            long snoozeDurationMillis) {
+        mNamespace = namespace;
+        mId = id;
+        mScore = score;
+        mCreationTimestampMillis = creationTimestampMillis;
+        mTtlMillis = ttlMillis;
+        mYear = year;
+        mMonth = month;
+        mDay = day;
+        mHour = hour;
+        mMinute = minute;
+        mStatus = status;
+        mSnoozeDurationMillis = snoozeDurationMillis;
+    }
+
+    /** Returns the namespace of the {@link AlarmInstance}. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the unique identifier of the {@link AlarmInstance}. */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the user-provided opaque document score of the {@link AlarmInstance}, which can be
+     * used for ranking using
+     * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}.
+     */
+    public int getScore() {
+        return mScore;
+    }
+
+    /**
+     * Returns the creation timestamp for the {@link AlarmInstance} document, in milliseconds
+     * using the {@link System#currentTimeMillis()} time base.
+     */
+    public long getCreationTimestampMillis() {
+        return mCreationTimestampMillis;
+    }
+
+    /**
+     * Returns the time-to-live (TTL) for the {@link AlarmInstance} document in milliseconds using
+     * the {@link System#currentTimeMillis()} time base.
+     *
+     * <p>The {@link AlarmInstance} document will be automatically deleted when the TTL expires.
+     */
+    public long getTtlMillis() {
+        return mTtlMillis;
+    }
+
+    /** Returns the year {@link AlarmInstance} is scheduled to fire. */
+    public int getYear() {
+        return mYear;
+    }
+
+    /**
+     * Returns the month {@link AlarmInstance} is scheduled to fire.
+     *
+     * <p>Month should range from {@link java.util.Calendar#JANUARY} to
+     * {@link java.util.Calendar#DECEMBER}.
+     */
+    @IntRange(from = Calendar.JANUARY, to = Calendar.DECEMBER)
+    public int getMonth() {
+        return mMonth;
+    }
+
+    /**
+     * Returns the day of the month {@link AlarmInstance} is scheduled to fire.
+     *
+     * <p>Days are specified by integers from 1 to 31.
+     */
+    @IntRange(from = 1, to = 31)
+    public int getDay() {
+        return mDay;
+    }
+
+    /**
+     * Returns the hour {@link AlarmInstance} is scheduled to fire.
+     *
+     * <p>Hours are specified by integers from 0 to 23.
+     */
+    @IntRange(from = 0, to = 23)
+    public int getHour() {
+        return mHour;
+    }
+
+    /**
+     * Returns the minute {@link AlarmInstance} is scheduled to fire.
+     *
+     * <p>Minutes are specified by integers from 0 to 59.
+     */
+    @IntRange(from = 0, to = 59)
+    public int getMinute() {
+        return mMinute;
+    }
+
+    /**
+     * Returns the current status of the {@link AlarmInstance}.
+     *
+     * <p>Status can be either {@link AlarmInstance#STATUS_UNKNOWN},
+     * {@link AlarmInstance#STATUS_SCHEDULED}, {@link AlarmInstance#STATUS_FIRING},
+     * {@link AlarmInstance#STATUS_DISMISSED}, {@link AlarmInstance#STATUS_SNOOZED}, or
+     * {@link AlarmInstance#STATUS_MISSED}.
+     */
+    @Status
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Returns the length of time in milliseconds the {@link AlarmInstance} will remain snoozed
+     * before it fires again, or -1 if the {@link AlarmInstance} does not support snoozing.
+     */
+    public long getSnoozeDurationMillis() {
+        return mSnoozeDurationMillis;
+    }
+
+    /** Builder for {@link AlarmInstance}. */
+    public static final class Builder {
+        private final String mNamespace;
+        private final String mId;
+        private int mScore;
+        private long mCreationTimestampMillis;
+        private long mTtlMillis;
+        private int mYear;
+        private int mMonth;
+        private int mDay;
+        private int mHour;
+        private int mMinute;
+        private int mStatus;
+        private long mSnoozeDurationMillis;
+
+        /**
+         * Constructor for {@link AlarmInstance.Builder}.
+         *
+         * @param namespace Namespace for the {@link AlarmInstance} Document. See
+         * {@link Document.Namespace}.
+         * @param id Unique identifier for the {@link AlarmInstance} Document. See
+         * {@link Document.Id}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mId = Preconditions.checkNotNull(id);
+
+            // Default for unset creationTimestampMillis. AppSearch will internally convert this
+            // to current time when creating the GenericDocument.
+            mCreationTimestampMillis = -1;
+            // default for snooze length. Indicates no snoozing.
+            mSnoozeDurationMillis = -1;
+        }
+
+        /**
+         * Constructor for {@link AlarmInstance.Builder} with all the existing values of an
+         * {@link AlarmInstance}.
+         */
+        public Builder(@NonNull AlarmInstance alarmInstance) {
+            this(alarmInstance.getNamespace(), alarmInstance.getId());
+            mScore = alarmInstance.getScore();
+            mCreationTimestampMillis = alarmInstance.getCreationTimestampMillis();
+            mTtlMillis = alarmInstance.getTtlMillis();
+            mYear = alarmInstance.getYear();
+            mMonth = alarmInstance.getMonth();
+            mDay = alarmInstance.getDay();
+            mHour = alarmInstance.getHour();
+            mMinute = alarmInstance.getMinute();
+            mStatus = alarmInstance.getStatus();
+            mSnoozeDurationMillis = alarmInstance.getSnoozeDurationMillis();
+        }
+
+        /**
+         * Sets the opaque document score of the {@link AlarmInstance}, which can be used for
+         * ranking using
+         * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}
+         *
+         * <p>See {@link Document.Score}
+         */
+        @NonNull
+        public Builder setScore(int score) {
+            mScore = score;
+            return this;
+        }
+
+        /**
+         * Sets the Creation Timestamp of the {@link AlarmInstance} document, in milliseconds
+         * using the {@link System#currentTimeMillis()} time base.
+         *
+         * <p>If not set, then the current timestamp will be used.
+         *
+         * <p>See {@link Document.CreationTimestampMillis}
+         */
+        @NonNull
+        public Builder setCreationTimestampMillis(long creationTimestampMillis) {
+            mCreationTimestampMillis = creationTimestampMillis;
+            return this;
+        }
+
+        /**
+         * Sets the time-to-live (TTL) for the {@link AlarmInstance} document in milliseconds using
+         * the {@link System#currentTimeMillis()} time base.
+         *
+         * <p>The {@link AlarmInstance} document will be automatically deleted when the TTL expires.
+         *
+         * <p>If set to 0, then the document will never expire.
+         *
+         * <p>See {@link Document.TtlMillis}
+         */
+        @NonNull
+        public Builder setTtlMillis(long ttlMillis) {
+            mTtlMillis = ttlMillis;
+            return this;
+        }
+
+        /** Sets the year {@link AlarmInstance} is scheduled to fire. */
+        @NonNull
+        public Builder setYear(int year) {
+            mYear = year;
+            return this;
+        }
+
+        /**
+         * Sets the month {@link AlarmInstance} is scheduled to fire.
+         *
+         * <p>Month should range from {@link java.util.Calendar#JANUARY} to
+         * {@link java.util.Calendar#DECEMBER}.
+         */
+        @NonNull
+        public Builder setMonth(
+                @IntRange(from = Calendar.JANUARY, to = Calendar.DECEMBER) int month) {
+            mMonth = Preconditions.checkArgumentInRange(month, Calendar.JANUARY,
+                    Calendar.DECEMBER, "month");
+            return this;
+        }
+
+        /**
+         * Sets the day of the month {@link AlarmInstance} is scheduled to fire.
+         *
+         * <p>Days are specified by integers from 1 to 31.
+         */
+        @NonNull
+        public Builder setDay(@IntRange(from = 1, to = 31) int day) {
+            mDay = Preconditions.checkArgumentInRange(day, 1, 31, "day");
+            return this;
+        }
+
+        /**
+         * Sets the hour {@link AlarmInstance} is scheduled to fire.
+         *
+         * <p>Hours are specified by integers from 0 to 23.
+         */
+        @NonNull
+        public Builder setHour(@IntRange(from = 0, to = 23) int hour) {
+            mHour = Preconditions.checkArgumentInRange(hour, 0, 23, "hour");
+            return this;
+        }
+
+        /**
+         * Sets the minute {@link AlarmInstance} is scheduled to fire.
+         *
+         * <p>Minutes are specified by integers from 0 to 59.
+         */
+        @NonNull
+        public Builder setMinute(@IntRange(from = 0, to = 59) int minute) {
+            mMinute = Preconditions.checkArgumentInRange(minute, 0, 59, "minute");
+            return this;
+        }
+
+        /**
+         * Sets the current status of the {@link AlarmInstance}.
+         *
+         * <p>Status can be either {@link AlarmInstance#STATUS_UNKNOWN},
+         * {@link AlarmInstance#STATUS_SCHEDULED}, {@link AlarmInstance#STATUS_FIRING},
+         * {@link AlarmInstance#STATUS_DISMISSED}, {@link AlarmInstance#STATUS_SNOOZED}, or
+         * {@link AlarmInstance#STATUS_MISSED}.
+         */
+        @NonNull
+        public Builder setStatus(@Status int status) {
+            mStatus = status;
+            return this;
+        }
+
+        /**
+         * Sets the length of time in milliseconds the {@link AlarmInstance} will remain snoozed
+         * before it fires again.
+         *
+         * <p>If not set, or set to -1, then the {@link AlarmInstance} does not support snoozing.
+         */
+        @NonNull
+        public Builder setSnoozeDurationMillis(long snoozeDurationMillis) {
+            mSnoozeDurationMillis = snoozeDurationMillis;
+            return this;
+        }
+
+        /** Builds the {@link AlarmInstance}. */
+        @NonNull
+        public AlarmInstance build() {
+            Preconditions.checkNotNull(mId);
+            Preconditions.checkNotNull(mNamespace);
+
+            return new AlarmInstance(mNamespace, mId, mScore, mCreationTimestampMillis, mTtlMillis,
+                    mYear, mMonth, mDay, mHour, mMinute, mStatus, mSnoozeDurationMillis);
+        }
+    }
+}
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
index 608c858..0bac43d 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.builtintypes;
 
+import android.os.SystemClock;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -30,7 +32,7 @@
 /**
  * AppSearch document representing a Timer entity.
  */
-@Document
+@Document(name = "builtin:Timer")
 public class Timer {
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -61,44 +63,53 @@
     @Document.Score
     private final int mScore;
 
+    @Document.CreationTimestampMillis
+    private final long mCreationTimestampMillis;
+
     @Document.TtlMillis
     private final long mTtlMillis;
 
-    @Document.StringProperty
-    private final String mRingtone;
-
     @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
     private final String mName;
 
     @Document.LongProperty
     private final long mDurationMillis;
 
-    @Document.BooleanProperty
-    private final boolean mVibrate;
+    @Document.LongProperty
+    private final long mStartTimeMillis;
+
+    @Document.LongProperty
+    private final long mStartTimeMillisInElapsedRealtime;
 
     @Document.LongProperty
     private final long mRemainingTimeMillis;
 
-    @Document.LongProperty
-    private final int mTimerStatus;
+    @Document.StringProperty
+    private final String mRingtone;
 
     @Document.LongProperty
-    private final long mExpireTimeMillis;
+    private final int mStatus;
 
-    Timer(String namespace, String id, int score, long ttlMillis, String ringtone,
-            String name, long durationMillis, boolean vibrate, long remainingTimeMillis,
-            int timerStatus, long expireTimeMillis) {
+    @Document.BooleanProperty
+    private final boolean mVibrate;
+
+    Timer(String namespace, String id, int score, long creationTimestampMillis, long ttlMillis,
+            String name, long durationMillis, long startTimeMillis,
+            long startTimeMillisInElapsedRealtime, long remainingTimeMillis, String ringtone,
+            int status, boolean vibrate) {
         mNamespace = namespace;
         mId = id;
         mScore = score;
+        mCreationTimestampMillis = creationTimestampMillis;
         mTtlMillis = ttlMillis;
-        mRingtone = ringtone;
         mName = name;
         mDurationMillis = durationMillis;
-        mVibrate = vibrate;
+        mStartTimeMillis = startTimeMillis;
+        mStartTimeMillisInElapsedRealtime = startTimeMillisInElapsedRealtime;
         mRemainingTimeMillis = remainingTimeMillis;
-        mTimerStatus = timerStatus;
-        mExpireTimeMillis = expireTimeMillis;
+        mRingtone = ringtone;
+        mStatus = status;
+        mVibrate = vibrate;
     }
 
     /** Returns the namespace of the {@link Timer}. */
@@ -123,7 +134,15 @@
     }
 
     /**
-     * Returns the TTL for the {@link Timer} document in milliseconds using the
+     * Returns the creation timestamp for the {@link Timer} document, in milliseconds using the
+     * {@link System#currentTimeMillis()} time base.
+     */
+    public long getCreationTimestampMillis() {
+        return mCreationTimestampMillis;
+    }
+
+    /**
+     * Returns the time-to-live (TTL) for the {@link Timer} document in milliseconds using the
      * {@link System#currentTimeMillis()} time base.
      *
      * <p>The {@link Timer} document will be automatically deleted when the TTL expires.
@@ -132,6 +151,80 @@
         return mTtlMillis;
     }
 
+    /** Returns the name associated with the {@link Timer}. */
+    @Nullable
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Returns the total duration of the {@link Timer}, in milliseconds.
+     */
+    public long getDurationMillis() {
+        return mDurationMillis;
+    }
+
+    /**
+     * Returns the time at which the {@link Timer} was started in milliseconds using the
+     * {@link System#currentTimeMillis()} time base.
+     *
+     *
+     * <p>If the {@link Timer} is in a {@link Timer#STATUS_STARTED} state, then its expire time
+     * can be calculated using:
+     * <pre>{@code
+     * long expireTime = timer.getStartTimeMillis + timer.getRemainingTimeMillis();
+     * }</pre>
+     *
+     * <p>See {@link #getStartTimeMillisInElapsedRealtime()} to see how startTimeMillis and
+     * startTimeMillisInElapsedRealtime should be used.
+     */
+    public long getStartTimeMillis() {
+        return mStartTimeMillis;
+    }
+
+    /**
+     * Returns the time at which the {@link Timer} was started in milliseconds using the
+     * {@link android.os.SystemClock#elapsedRealtime()} time base, or -1 if not present.
+     *
+     * <p>If present, startTimeMillisInElapsedRealtime should be the preferred value used to do
+     * accurate time keeping in {@link Timer}.
+     *
+     * <p>If not present, or if {@link SystemClock#elapsedRealtime()} is unreliable, for example
+     * after a device reboot, or the {@link Timer} document is moved to a different device, then
+     * startTimeMillis should be used instead for time keeping.
+     *
+     * <p>If the {@link Timer} is in a {@link Timer#STATUS_STARTED} state, then its expire time
+     * can be calculated using:
+     * <pre>{@code
+     * long elapsedTime = SystemClock.elapsedRealtime() -
+     *   timer.getStartTimeMillisInElapsedRealtime();
+     * long expireTime = System.currentTimeMillis() + timer.getRemainingTimeMillis() - elapsedTime;
+     * }</pre>
+     */
+    public long getStartTimeMillisInElapsedRealtime() {
+        return mStartTimeMillisInElapsedRealtime;
+    }
+
+    /**
+     * Returns the amount of time remaining when the {@link Timer} was started, paused or reset,
+     * in milliseconds.
+     *
+     * <p>The current remaining time can also be calculate using either
+     * {@link #getStartTimeMillis()} or {@link #getStartTimeMillisInElapsedRealtime()}:
+     * <pre>{@code
+     * long elapsedTime = System.currentTimeMillis() - timer.getStartTimeMillis();
+     * long currentRemainingTime = timer.getRemainingTimeMillis() - elapsedTime;
+     * }</pre>
+     * <pre>{@code
+     * long elapsedTime = SystemClock.elapsedRealtime() -
+     *   timer.getStartTimeMillisInElapsedRealtime();
+     * long currentRemainingTime = timer.getRemainingTimeMillis() - elapsedTime;
+     * }</pre>
+     */
+    public long getRemainingTimeMillis() {
+        return mRemainingTimeMillis;
+    }
+
     /**
      * Returns the ringtone of the {@link Timer} as a content URI to be played, or
      * {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
@@ -141,33 +234,6 @@
         return mRingtone;
     }
 
-    /** Returns the name associated with the {@link Timer}. */
-    @Nullable
-    public String getName() {
-        return mName;
-    }
-
-    /**
-     * Returns the total duration of the {@link Timer} when it was first created, in milliseconds
-     * using the {@link System#currentTimeMillis()} time base.
-     */
-    public long getDurationMillis() {
-        return mDurationMillis;
-    }
-
-    /** Returns whether or not to activate the device vibrator when the {@link Timer} expires. */
-    public boolean isVibrate() {
-        return mVibrate;
-    }
-
-    /**
-     * Returns the amount of time remaining when the {@link Timer} was started or paused, in
-     * milliseconds using the {@link System#currentTimeMillis()} time base.
-     */
-    public long getRemainingTimeMillis() {
-        return mRemainingTimeMillis;
-    }
-
     /**
      * Returns the current status of the {@link Timer}.
      *
@@ -176,19 +242,13 @@
      * {@link Timer#STATUS_RESET}.
      */
     @Status
-    public int getTimerStatus() {
-        return mTimerStatus;
+    public int getStatus() {
+        return mStatus;
     }
 
-    /**
-     * Returns the time at which the {@link Timer} will, or did expire in milliseconds since
-     * epoch.
-     *
-     * <p>Unlike {@link Timer#getTtlMillis()}, the {@link Timer} document will not be
-     * automatically deleted when the expire time is reached.
-     */
-    public long getExpireTimeMillis() {
-        return mExpireTimeMillis;
+    /** Returns whether or not to activate the device vibrator when the {@link Timer} expires. */
+    public boolean isVibrate() {
+        return mVibrate;
     }
 
     /** Builder for {@link Timer}. */
@@ -196,25 +256,51 @@
         private final String mNamespace;
         private final String mId;
         private int mScore;
+        private long mCreationTimestampMillis;
         private long mTtlMillis;
-        private String mRingtone;
         private String mName;
         private long mDurationMillis;
-        private boolean mVibrate;
+        private long mStartTimeMillis;
+        private long mStartTimeMillisInElapsedRealtime;
         private long mRemainingTimeMillis;
-        private int mTimerStatus;
-        private long mExpireTimeMillis;
+        private String mRingtone;
+        private int mStatus;
+        private boolean mVibrate;
 
         /**
          * Constructor for {@link Timer.Builder}.
          *
-         * @param id Unique identifier for the {@link Timer} Document. See {@link Document.Id}.
          * @param namespace Namespace for the {@link Timer} Document. See
          * {@link Document.Namespace}.
+         * @param id Unique identifier for the {@link Timer} Document. See {@link Document.Id}.
          */
         public Builder(@NonNull String namespace, @NonNull String id) {
             mNamespace = Preconditions.checkNotNull(namespace);
             mId = Preconditions.checkNotNull(id);
+
+            // Default for unset creationTimestampMillis. AppSearch will internally convert this
+            // to current time when creating the GenericDocument.
+            mCreationTimestampMillis = -1;
+            // Default for unset startTimeMillisInElapsedRealtime
+            mStartTimeMillisInElapsedRealtime = -1;
+        }
+
+        /**
+         * Constructor for {@link Timer.Builder} with all the existing values of a {@link Timer}.
+         */
+        public Builder(@NonNull Timer timer) {
+            this(timer.getNamespace(), timer.getId());
+            mScore = timer.getScore();
+            mCreationTimestampMillis = timer.getCreationTimestampMillis();
+            mTtlMillis = timer.getTtlMillis();
+            mName = timer.getName();
+            mDurationMillis = timer.getDurationMillis();
+            mStartTimeMillis = timer.getStartTimeMillis();
+            mStartTimeMillisInElapsedRealtime = timer.getStartTimeMillisInElapsedRealtime();
+            mRemainingTimeMillis = timer.getRemainingTimeMillis();
+            mRingtone = timer.getRingtone();
+            mStatus = timer.getStatus();
+            mVibrate = timer.isVibrate();
         }
 
         /**
@@ -230,12 +316,26 @@
         }
 
         /**
-         * Sets the TTL for the {@link Timer} document in milliseconds using the
+         * Sets the creation timestamp of the {@link Timer} document, in milliseconds using the
+         * {@link System#currentTimeMillis()} time base.
+         *
+         * <p>If not set, then the current timestamp will be used.
+         *
+         * <p>See {@link Document.CreationTimestampMillis}
+         */
+        @NonNull
+        public Builder setCreationTimestampMillis(long creationTimestampMillis) {
+            mCreationTimestampMillis = creationTimestampMillis;
+            return this;
+        }
+
+        /**
+         * Sets the time-to-live (TTL) for the {@link Timer} document in milliseconds using the
          * {@link System#currentTimeMillis()} time base.
          *
          * <p>The {@link Timer} document will be automatically deleted when the TTL expires.
          *
-         * <p>If set to 0, then the document will never expire.
+         * <p>If not set, then the document will never expire.
          *
          * <p>See {@link Document.TtlMillis}
          */
@@ -245,6 +345,64 @@
             return this;
         }
 
+        /** Sets the name. */
+        @NonNull
+        public Builder setName(@Nullable String name) {
+            mName = name;
+            return this;
+        }
+
+        /**
+         * Sets the total duration of the {@link Timer}, in milliseconds.
+         */
+        @NonNull
+        public Builder setDurationMillis(long durationMillis) {
+            mDurationMillis = durationMillis;
+            return this;
+        }
+
+        /**
+         * Sets the time at which the {@link Timer} was started in milliseconds using the
+         * {@link System#currentTimeMillis()} time base.
+         *
+         * <p>See {@link #setStartTimeMillisInElapsedRealtime(long)} on how startTimeMillis and
+         * startTimeMillisInElapsedRealtime should be used.
+         */
+        @NonNull
+        public Builder setStartTimeMillis(long startTimeMillis) {
+            mStartTimeMillis = startTimeMillis;
+            return this;
+        }
+
+        /**
+         * Sets the time at which the {@link Timer} was started in milliseconds using the
+         * {@link android.os.SystemClock#elapsedRealtime()} time base.
+         *
+         * <p>startTimeMillis and startTimeMillisInElapsedRealtime should be sampled at
+         * the same time, using {@link System#currentTimeMillis()} and
+         * {@link android.os.SystemClock#elapsedRealtime()} respectively.
+         *
+         * <p>In situations where the reader cannot reliably use
+         * {@link android.os.SystemClock#elapsedRealtime()}, for example if the reader is not on
+         * the same device where the {@link Timer} document is written, then
+         * startTimeMillisInElapsedRealtime should not be set.
+         */
+        @NonNull
+        public Builder setStartTimeMillisInElapsedRealtime(long startTimeMillisInElapsedRealtime) {
+            mStartTimeMillisInElapsedRealtime = startTimeMillisInElapsedRealtime;
+            return this;
+        }
+
+        /**
+         * Sets the amount of time remaining when the {@link Timer} was started, paused or reset,
+         * in milliseconds.
+         */
+        @NonNull
+        public Builder setRemainingTimeMillis(long remainingTimeMillis) {
+            mRemainingTimeMillis = remainingTimeMillis;
+            return this;
+        }
+
         /**
          * Sets the content URI for the ringtone to be played, or
          * {@link android.provider.AlarmClock#VALUE_RINGTONE_SILENT} if no ringtone will be played.
@@ -255,20 +413,16 @@
             return this;
         }
 
-        /** Sets the name. */
-        @NonNull
-        public Builder setName(@Nullable String name) {
-            mName = name;
-            return this;
-        }
-
         /**
-         * Sets the total duration of the {@link Timer} when it was first created in milliseconds
-         * using the {@link System#currentTimeMillis()} time base.
+         * Sets the current status of the {@link Timer}.
+         *
+         * <p>Status can be {@link Timer#STATUS_UNKNOWN}, {@link Timer#STATUS_STARTED},
+         * {@link Timer#STATUS_PAUSED}, {@link Timer#STATUS_EXPIRED}, {@link Timer#STATUS_MISSED},
+         * or {@link Timer#STATUS_RESET}.
          */
         @NonNull
-        public Builder setDurationMillis(long durationMillis) {
-            mDurationMillis = durationMillis;
+        public Builder setStatus(@Status int status) {
+            mStatus = status;
             return this;
         }
 
@@ -279,52 +433,15 @@
             return this;
         }
 
-        /**
-         * Sets the amount of time remaining when the {@link Timer} was started or paused, in
-         * milliseconds using the {@link System#currentTimeMillis()} time base.
-         */
-        @NonNull
-        public Builder setRemainingTimeMillis(long remainingTimeMillis) {
-            mRemainingTimeMillis = remainingTimeMillis;
-            return this;
-        }
-
-        /**
-         * Sets the current status of the {@link Timer}.
-         *
-         * @param timerStatus Can be {@link Timer#STATUS_UNKNOWN}, {@link Timer#STATUS_STARTED},
-         * {@link Timer#STATUS_PAUSED}, {@link Timer#STATUS_EXPIRED}, {@link Timer#STATUS_MISSED}
-         *                    , or {@link Timer#STATUS_RESET}.
-         */
-        @NonNull
-        public Builder setTimerStatus(@Status int timerStatus) {
-            mTimerStatus = timerStatus;
-            return this;
-        }
-
-        /**
-         * Sets the time at which the {@link Timer} will, or did expire in milliseconds since epoch.
-         *
-         * <p>If set to 0, then the {@link Timer} will never expire.
-         *
-         * <p>Unlike {@link Builder#setTtlMillis(long)}, the {@link Timer} document will not be
-         * automatically deleted when the expire time is reached.
-         */
-        @NonNull
-        public Builder setExpireTimeMillis(long expireTimeMillis) {
-            mExpireTimeMillis = expireTimeMillis;
-            return this;
-        }
-
         /** Builds the {@link Timer}. */
         @NonNull
         public Timer build() {
             Preconditions.checkNotNull(mId);
             Preconditions.checkNotNull(mNamespace);
 
-            return new Timer(mNamespace, mId, mScore, mTtlMillis, mRingtone, mName,
-                    mDurationMillis, mVibrate, mRemainingTimeMillis, mTimerStatus,
-                    mExpireTimeMillis);
+            return new Timer(mNamespace, mId, mScore, mCreationTimestampMillis, mTtlMillis, mName,
+                    mDurationMillis, mStartTimeMillis, mStartTimeMillisInElapsedRealtime,
+                    mRemainingTimeMillis, mRingtone, mStatus, mVibrate);
         }
     }
 }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
index 2920535..cfe4150 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Outputs.kt
@@ -52,9 +52,9 @@
         @Suppress("DEPRECATION")
         @SuppressLint("NewApi")
         dirUsableByAppAndShell = when {
-            Build.VERSION.SDK_INT in 30..31 -> {
-                // On Android R, S we are using the media directory because that is the directory
-                // that the shell has access to. Context: b/181601156
+            Build.VERSION.SDK_INT in 29..31 -> {
+                // On Android Q, R and S we are using the media directory because that is
+                // the directory that the shell has access to. Context: b/181601156
                 InstrumentationRegistry.getInstrumentation().context.getFirstMountedMediaDir()
             }
             Build.VERSION.SDK_INT <= 22 -> {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
index 3cdaa64..d897d5d 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
@@ -32,6 +32,11 @@
     val tag: String
 ) {
     ActivityManager("am"),
+    Audio("audio") {
+        override fun supported(api: Int, rooted: Boolean): Boolean {
+            return api >= 23
+        }
+    },
     BinderDriver("binder_driver") {
         override fun supported(api: Int, rooted: Boolean): Boolean {
             return api >= 24
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 2dd0f5f..3b96d05 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -57,6 +57,7 @@
             ),
             atrace_categories = listOf(
                 AtraceTag.ActivityManager,
+                AtraceTag.Audio,
                 AtraceTag.BinderDriver,
                 AtraceTag.Camera,
                 AtraceTag.Dalvik,
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index 3292f02..32672e61 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -4,6 +4,10 @@
   @RequiresApi(29) public final class Api29Kt {
   }
 
+  @RequiresApi(23) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AudioUnderrunMetric extends androidx.benchmark.macro.Metric {
+    ctor public AudioUnderrunMetric();
+  }
+
   public enum BaselineProfileMode {
     enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
     enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
new file mode 100644
index 0000000..f3fd1ca
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.benchmark.macro.perfetto
+
+import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+@SdkSuppress(minSdkVersion = 23)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class AudioUnderrunQueryTest {
+    @Test
+    fun validateFixedTrace() {
+        assumeTrue(isAbiSupported())
+
+        // the trace was generated during 2 seconds AudioUnderrunBenchmark scenario run
+        val traceFile = createTempFileFromAsset("api23_audio_underrun", ".perfetto-trace")
+
+        val subMetrics = AudioUnderrunQuery.getSubMetrics(traceFile.absolutePath)
+        val expectedMetrics = AudioUnderrunQuery.SubMetrics(2212, 892)
+
+        assertEquals(expectedMetrics, subMetrics)
+    }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 135615c..bca9891 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -353,15 +353,15 @@
          * Represents the default compilation mode for the platform, on an end user's device.
          *
          * This is a post-store-install app configuration for this device's SDK
-         * version - [`Partial(BaselineProfileMode.IncludeIfAvailable)`][Partial] on
-         * API 24+, and [Full] prior to API 24 (where all apps are fully AOT compiled).
+         * version - [`Partial(BaselineProfileMode.UseIfAvailable)`][Partial] on API 24+, and
+         * [Full] prior to API 24 (where all apps are fully AOT compiled).
          *
          * On API 24+, Baseline Profile pre-compilation is used if possible, but no error will be
          * thrown if installation fails.
          *
          * Generally, it is preferable to explicitly pass a compilation mode, such as
-         * [Partial(BaselineProfileMode.Include)][Partial] to avoid ambiguity, and e.g. validate an
-         * app's BaselineProfile can be correctly used.
+         * [`Partial(BaselineProfileMode.Required)`][Partial] to avoid ambiguity, and e.g. validate
+         * an app's BaselineProfile can be correctly used.
          */
         @JvmField
         val DEFAULT: CompilationMode = if (Build.VERSION.SDK_INT >= 24) {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 84ed44d..242b31e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.benchmark.Shell
+import androidx.benchmark.macro.perfetto.AudioUnderrunQuery
 import androidx.benchmark.macro.perfetto.FrameTimingQuery
 import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric
 import androidx.benchmark.macro.perfetto.PerfettoResultsParser.parseStartupResult
@@ -56,6 +57,50 @@
 private fun Long.nsToDoubleMs(): Double = this / 1_000_000.0
 
 /**
+ * Metric which captures information about playing audio.
+ *
+ * Each time an instance of [android.media.AudioTrack] is started, the systems repeatedly
+ * logs the number of audio frames available for output. This doesn't work when audio offload is
+ * enabled. No logs are generated while there is no active track.
+ *
+ * Test fails in case of multiple active tracks during a single iteration.
+ *
+ * This outputs the following measurements:
+ *
+ * * `totalMs` - Total duration of played audio captured during the iteration.
+ * The test fails if no counters are detected.
+ *
+ * * `zeroMs` - Duration of played audio when zero audio frames were available for output. Each
+ * counter with zero frames indicates a gap in audio playing.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Suppress("CanSealedSubClassBeObject")
+@RequiresApi(23)
+public class AudioUnderrunMetric : Metric() {
+    internal override fun configure(packageName: String) {
+    }
+
+    internal override fun start() {
+    }
+
+    internal override fun stop() {
+    }
+
+    internal override fun getMetrics(captureInfo: CaptureInfo, tracePath: String): IterationResult {
+        val subMetrics = AudioUnderrunQuery.getSubMetrics(tracePath)
+
+        return IterationResult(
+            singleMetrics = mapOf(
+                "totalMs" to subMetrics.totalMs.toDouble(),
+                "zeroMs" to subMetrics.zeroMs.toDouble()
+            ),
+            sampledMetrics = emptyMap(),
+            timelineRangeNs = null
+        )
+    }
+}
+
+/**
  * Legacy version of FrameTimingMetric, based on 'dumpsys gfxinfo' instead of trace data.
  *
  * Temporary - to be removed after transition to FrameTimingMetric
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
new file mode 100644
index 0000000..12db8c3
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.benchmark.macro.perfetto
+
+internal object AudioUnderrunQuery {
+    private fun getFullQuery() = """
+        SELECT track.name, counter.value, counter.ts
+        FROM track
+        JOIN counter ON track.id = counter.track_id
+        WHERE track.type = 'process_counter_track' AND track.name LIKE 'nRdy%'
+    """.trimIndent()
+
+    data class SubMetrics(
+        val totalMs: Int,
+        val zeroMs: Int
+    )
+
+    fun getSubMetrics(
+        absoluteTracePath: String
+    ): SubMetrics {
+        val queryResult = PerfettoTraceProcessor.rawQuery(
+            absoluteTracePath = absoluteTracePath,
+            query = getFullQuery()
+        )
+
+        val resultLines = queryResult.split("\n")
+
+        if (resultLines.first() != "\"name\",\"value\",\"ts\"") {
+            throw IllegalStateException("query failed!")
+        }
+
+        // we can't measure duration when there is only one time stamp
+        if (resultLines.size <= 3) {
+            throw RuntimeException("No playing audio detected")
+        }
+
+        var trackName: String? = null
+        var lastTs: Long? = null
+
+        var totalNs: Long = 0
+        var zeroNs: Long = 0
+
+        resultLines
+            .drop(1) // column names
+            .dropLast(1) // empty line
+            .forEach {
+                val lineVals = it.split(",")
+                if (lineVals.size != VAL_MAX)
+                    throw IllegalStateException("query failed")
+
+                if (trackName == null) {
+                    trackName = lineVals[VAL_NAME]
+                } else if (!trackName.equals(lineVals[VAL_NAME])) {
+                    throw RuntimeException("There could be only one AudioTrack per measure")
+                }
+
+                if (lastTs == null) {
+                    lastTs = lineVals[VAL_TS].toLong()
+                } else {
+                    val frameNs = lineVals[VAL_TS].toLong() - lastTs!!
+                    lastTs = lineVals[VAL_TS].toLong()
+
+                    totalNs += frameNs
+
+                    val frameCounter = lineVals[VAL_VALUE].toDouble().toInt()
+
+                    if (frameCounter == 0)
+                        zeroNs += frameNs
+                }
+            }
+
+        return SubMetrics((totalNs / 1_000_000).toInt(), (zeroNs / 1_000_000).toInt())
+    }
+
+    private const val VAL_NAME = 0
+    private const val VAL_VALUE = 1
+    private const val VAL_TS = 2
+    private const val VAL_MAX = 3
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 80943c0..69c5a3b 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -62,6 +62,15 @@
             </intent-filter>
         </activity>
 
+        <activity
+            android:name=".AudioActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.macrobenchmark.target.AUDIO_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
         <!--
         Activities need to be exported so the macrobenchmark can discover them
          -->
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/AudioActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/AudioActivity.kt
new file mode 100644
index 0000000..752e908
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/AudioActivity.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.benchmark.integration.macrobenchmark.target
+
+import android.media.AudioAttributes
+import android.media.AudioFormat
+import android.media.AudioTrack
+import android.os.Build
+import android.os.Bundle
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import kotlin.concurrent.thread
+import kotlin.math.sin
+
+@RequiresApi(Build.VERSION_CODES.M)
+class AudioActivity() : AppCompatActivity() {
+    private lateinit var thread: Thread
+
+    @Synchronized get
+    @Synchronized set
+    private var finished = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_audio)
+
+        findViewById<TextView>(R.id.audioTextNotice).setText(R.string.audio_notice)
+
+        val sampleRateHz = 22050
+        val bufferDurationMs = 250
+        val buffer = generateBuffer(bufferDurationMs, 500, sampleRateHz)
+
+        // plays beeps continuously until activity is destroyed
+        thread = thread {
+            var format = AudioFormat.Builder()
+                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                .setSampleRate(sampleRateHz)
+                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+                .build()
+
+            var attributes = AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                .build()
+
+            val track = AudioTrack.Builder()
+                .setAudioAttributes(attributes)
+                .setAudioFormat(format)
+                .setBufferSizeInBytes(buffer.size * 2)
+                .build()
+
+            track.play()
+
+            while (!finished) {
+                var currentTime = System.currentTimeMillis()
+                track.write(buffer, 0, buffer.size)
+
+                // sleep twice as buffer duration to generate pauses
+                val targetTime = currentTime + bufferDurationMs * 2
+                Thread.sleep(targetTime - System.currentTimeMillis())
+            }
+
+            track.stop()
+        }
+    }
+
+    override fun onDestroy() {
+        finished = true
+        thread.join()
+
+        super.onDestroy()
+    }
+
+    private fun generateBuffer(durationMs: Int, frequency: Int, sampleRateHz: Int): ShortArray {
+        val numSamples = durationMs * sampleRateHz / 1000
+
+        val buffer = ShortArray(numSamples)
+        for (i in 0 until numSamples) {
+            val sample = sin(2 * Math.PI * i / (sampleRateHz / frequency)) * 0.1
+            buffer[i] = (sample * Short.MAX_VALUE).toInt().toShort()
+        }
+
+        return buffer
+    }
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_audio.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_audio.xml
new file mode 100644
index 0000000..0dc66b9
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_audio.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/audioTextNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="text" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml
index f8753b2..8aaaec8 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/values/donottranslate-strings.xml
@@ -17,4 +17,5 @@
 <resources>
     <string name="app_notice">Macrobenchmark Integration Test App.</string>
     <string name="recyclerDescription">A list of items</string>
+    <string name="audio_notice">Repeatedly plays beeps</string>
 </resources>
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
new file mode 100644
index 0000000..ab94e5c
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.AudioUnderrunMetric
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 23)
+class AudioUnderrunBenchmark() {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    private lateinit var device: UiDevice
+
+    @Before
+    fun setUp() {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        device = UiDevice.getInstance(instrumentation)
+    }
+
+    @Test
+    fun start() {
+        benchmarkRule.measureRepeated(
+            packageName = PACKAGE_NAME,
+            metrics = listOf(AudioUnderrunMetric()),
+            compilationMode = CompilationMode.Full(),
+            startupMode = StartupMode.WARM,
+            iterations = 1,
+            setupBlock = {
+                val intent = Intent()
+                intent.action = ACTION
+                startActivityAndWait(intent)
+            }
+        ) {
+            // audio is played for a half of duration, ~50% of the frames would be zero
+            Thread.sleep(DURATION_MS.toLong())
+        }
+    }
+
+    companion object {
+        private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
+        private const val ACTION = "$PACKAGE_NAME.AUDIO_ACTIVITY"
+        private const val DURATION_MS = 2000
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 53d7c42..6345639 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -72,6 +72,7 @@
     // Create fake variant tasks since that is what is invoked by developers.
     val lintTask = tasks.named("lint")
     lintTask.configure { task ->
+        task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
         AffectedModuleDetector.configureTaskGuard(task)
     }
     afterEvaluate {
@@ -122,6 +123,7 @@
                 }}"
             ).configure { task ->
                 AffectedModuleDetector.configureTaskGuard(task)
+                task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
             }
             tasks.named(
                 "lintAnalyze${variant.name.replaceFirstChar {
@@ -129,6 +131,7 @@
                 }}"
             ).configure { task ->
                 AffectedModuleDetector.configureTaskGuard(task)
+                task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText"))
             }
             /* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04
             tasks.named("lintReport${variant.name.capitalize(Locale.US)}").configure { task ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
index 67e3c24..ef9d034 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
@@ -153,10 +153,15 @@
         val args = DoclavaArgumentBuilder()
 
         // classpath
-        val classpathString = classpath!!.files.map({ f -> f.toString() }).joinToString(":")
-        args.addStringOption("cp", classpathString)
+        val classpathFile = File.createTempFile("doclavaClasspath", ".txt")
+        classpathFile.deleteOnExit()
+        classpathFile.bufferedWriter().use { writer ->
+            val classpathString = classpath!!.files.map({ f -> f.toString() }).joinToString(":")
+            writer.write(classpathString)
+        }
+        args.addStringOption("cp", "@$classpathFile")
         args.addStringOption("doclet", "com.google.doclava.Doclava")
-        args.addStringOption("docletpath", classpathString)
+        args.addStringOption("docletpath", "@$classpathFile")
 
         args.addOption("quiet")
         args.addStringOption("encoding", "UTF-8")
@@ -197,15 +202,21 @@
         args.addFileOption("d", destinationDir!!)
 
         // source files
-        for (source in sources) {
-            for (file in source) {
-                val arg = file.toString()
-                // Doclava does not know how to parse Kotlin files
-                if (!arg.endsWith(".kt")) {
-                    args.add(arg)
+        val tmpArgs = File.createTempFile("doclavaSourceArgs", ".txt")
+        tmpArgs.deleteOnExit()
+        tmpArgs.bufferedWriter().use { writer ->
+            for (source in sources) {
+                for (file in source) {
+                    val arg = file.toString()
+                    // Doclava does not know how to parse Kotlin files
+                    if (!arg.endsWith(".kt")) {
+                        writer.write(arg)
+                        writer.newLine()
+                    }
                 }
             }
         }
+        args.add("@$tmpArgs")
 
         return args.build() + extraArgumentsBuilder.build()
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 3e011f6..1d0b2c7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -111,7 +111,7 @@
             unzippedSamplesSources,
             samplesSourcesConfiguration
         )
-        val unzippedDocsSources = File(project.buildDir, "unzippedDocsSources")
+        val unzippedDocsSources = File(project.buildDir, "srcs")
         val unzipDocsTask = configureUnzipTask(
             project,
             "unzipDocsSources",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt
index 0bd6f66..3787ab1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt
@@ -171,11 +171,6 @@
     }
 
     override fun StudioTask.updateJvmHeapSize() {
-        val vmoptions =
-            File(binaryDirectory, "bin/studio.vmoptions")
-        val newText = vmoptions.readText().replace(jvmHeapRegex, "-Xmx4g")
-        vmoptions.writeText(newText)
-
         val vmoptions64 =
             File(binaryDirectory, "bin/studio64.vmoptions")
         val newText64 = vmoptions64.readText().replace(jvmHeapRegex, "-Xmx8g")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index 5579270..b5d5239 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -85,6 +85,12 @@
         checkApi = RunApiTasks.No("Lint Library"),
         compilationTarget = CompilationTarget.HOST
     ),
+    COMPILER_DAEMON(
+        Publish.SNAPSHOT_AND_RELEASE,
+        sourceJars = false,
+        RunApiTasks.No("Compiler Daemon (Host-only)"),
+        CompilationTarget.HOST
+    ),
     COMPILER_PLUGIN(
         Publish.SNAPSHOT_AND_RELEASE,
         sourceJars = false,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
index f3f6540..3b4ae6a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -27,7 +27,7 @@
 import androidx.camera.camera2.pipe.integration.impl.CameraProperties
 import androidx.camera.core.ExposureState
 
-internal val EMPTY_RANGE = Range(0, 0)
+internal val EMPTY_RANGE: Range<Int> = Range(0, 0)
 
 /** Adapt [ExposureState] to a [CameraMetadata] instance. */
 @SuppressLint("UnsafeOptInUsageError")
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
index 452bc8e..7324125 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
@@ -61,7 +61,7 @@
     }
 }
 
-internal val EMPTY_RANGE = Range(0, 0)
+internal val EMPTY_RANGE: Range<Int> = Range(0, 0)
 
 /**
  * The implementation of the [EvCompCompat]. The [applyAsync] update the new exposure index value
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
index 1c091d8..41e8e81 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
@@ -34,24 +34,25 @@
 import androidx.camera.core.impl.OptionsBundle
 
 internal const val CAPTURE_REQUEST_ID_STEM = "camera2.captureRequest.option."
-internal val TEMPLATE_TYPE_OPTION = Config.Option.create<Int>(
+internal val TEMPLATE_TYPE_OPTION: Config.Option<Int> = Config.Option.create(
     "camera2.captureRequest.templateType",
     Int::class.javaPrimitiveType!!
 )
-internal val DEVICE_STATE_CALLBACK_OPTION = Config.Option.create<CameraDevice.StateCallback>(
-    "camera2.cameraDevice.stateCallback",
-    CameraDevice.StateCallback::class.java
-)
-internal val SESSION_STATE_CALLBACK_OPTION =
-    Config.Option.create<CameraCaptureSession.StateCallback>(
+internal val DEVICE_STATE_CALLBACK_OPTION: Config.Option<CameraDevice.StateCallback> =
+    Config.Option.create(
+        "camera2.cameraDevice.stateCallback",
+        CameraDevice.StateCallback::class.java
+    )
+internal val SESSION_STATE_CALLBACK_OPTION: Config.Option<CameraCaptureSession.StateCallback> =
+    Config.Option.create(
         "camera2.cameraCaptureSession.stateCallback",
         CameraCaptureSession.StateCallback::class.java
     )
-internal val SESSION_CAPTURE_CALLBACK_OPTION = Config.Option.create<CaptureCallback>(
+internal val SESSION_CAPTURE_CALLBACK_OPTION: Config.Option<CaptureCallback> = Config.Option.create(
     "camera2.cameraCaptureSession.captureCallback",
     CaptureCallback::class.java
 )
-internal val CAPTURE_REQUEST_TAG_OPTION = Config.Option.create<Any>(
+internal val CAPTURE_REQUEST_TAG_OPTION: Config.Option<Any> = Config.Option.create(
     "camera2.captureRequest.tag", Any::class.java
 )
 // TODO: Porting the CameraEventCallback option constant.
diff --git a/camera/camera-camera2/api/public_plus_experimental_current.txt b/camera/camera-camera2/api/public_plus_experimental_current.txt
index 290eb9c..583ba10 100644
--- a/camera/camera-camera2/api/public_plus_experimental_current.txt
+++ b/camera/camera-camera2/api/public_plus_experimental_current.txt
@@ -11,6 +11,7 @@
 
   @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraControl {
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> addCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> clearCaptureRequestOptions();
     method public static androidx.camera.camera2.interop.Camera2CameraControl from(androidx.camera.core.CameraControl);
     method public androidx.camera.camera2.interop.CaptureRequestOptions getCaptureRequestOptions();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
index 75a6bb8..b94be04 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
@@ -25,6 +25,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.compat.workaround.MaxPreviewSize;
 
 /**
  * A singleton class to retrieve display related information.
@@ -37,6 +38,8 @@
     @NonNull
     private final DisplayManager mDisplayManager;
     private volatile Size mPreviewSize = null;
+    private final MaxPreviewSize mMaxPreviewSize = new MaxPreviewSize();
+
     private DisplayInfoManager(@NonNull Context context) {
         mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
     }
@@ -125,9 +128,8 @@
 
         if (displayViewSize.getWidth() * displayViewSize.getHeight()
                 > MAX_PREVIEW_SIZE.getWidth() * MAX_PREVIEW_SIZE.getHeight()) {
-            return MAX_PREVIEW_SIZE;
-        } else {
-            return displayViewSize;
+            displayViewSize = MAX_PREVIEW_SIZE;
         }
+        return mMaxPreviewSize.getMaxPreviewResolution(displayViewSize);
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index e387700..bbbe4d1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -38,6 +38,7 @@
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
 import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
 import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
+import androidx.camera.camera2.internal.compat.workaround.ResolutionSelector;
 import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.CameraUnavailableException;
@@ -100,6 +101,7 @@
     private Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
     @NonNull
     private final DisplayInfoManager mDisplayInfoManager;
+    private final ResolutionSelector mResolutionSelector = new ResolutionSelector();
 
     SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
             @NonNull CameraManagerCompat cameraManagerCompat,
@@ -168,25 +170,9 @@
      * @return new {@link SurfaceConfig} object
      */
     SurfaceConfig transformSurfaceConfig(int imageFormat, Size size) {
-        ConfigType configType;
+        ConfigType configType = getConfigType(imageFormat);
         ConfigSize configSize = ConfigSize.NOT_SUPPORT;
 
-        /*
-         * PRIV refers to any target whose available sizes are found using
-         * StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format,
-         * YUV refers to a target Surface using the ImageFormat.YUV_420_888 format, JPEG refers to
-         * the ImageFormat.JPEG format, and RAW refers to the ImageFormat.RAW_SENSOR format.
-         */
-        if (imageFormat == ImageFormat.YUV_420_888) {
-            configType = ConfigType.YUV;
-        } else if (imageFormat == ImageFormat.JPEG) {
-            configType = ConfigType.JPEG;
-        } else if (imageFormat == ImageFormat.RAW_SENSOR) {
-            configType = ConfigType.RAW;
-        } else {
-            configType = ConfigType.PRIV;
-        }
-
         Size maxSize = fetchMaxSize(imageFormat);
 
         // Compare with surface size definition to determine the surface configuration size
@@ -301,7 +287,7 @@
         // Gets the corrected aspect ratio due to device constraints or null if no correction is
         // needed.
         @TargetAspectRatio.Ratio int targetAspectRatio =
-                new TargetAspectRatio().get(imageOutputConfig, mCameraId, mCharacteristics);
+                new TargetAspectRatio().get(mCameraId, mCharacteristics);
         switch (targetAspectRatio) {
             case TargetAspectRatio.RATIO_4_3:
                 outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3 : ASPECT_RATIO_3_4;
@@ -492,9 +478,36 @@
             }
         }
 
+        supportedResolutions = mResolutionSelector.insertOrPrioritize(
+                getConfigType(config.getInputFormat()),
+                supportedResolutions);
+
         return supportedResolutions;
     }
 
+
+    /**
+     * Gets {@link ConfigType} from image format.
+     *
+     * <p> PRIV refers to any target whose available sizes are found using
+     * StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format,
+     * YUV refers to a target Surface using the ImageFormat.YUV_420_888 format, JPEG refers to
+     * the ImageFormat.JPEG format, and RAW refers to the ImageFormat.RAW_SENSOR format.
+     */
+    @NonNull
+    private SurfaceConfig.ConfigType getConfigType(int imageFormat) {
+
+        if (imageFormat == ImageFormat.YUV_420_888) {
+            return SurfaceConfig.ConfigType.YUV;
+        } else if (imageFormat == ImageFormat.JPEG) {
+            return SurfaceConfig.ConfigType.JPEG;
+        } else if (imageFormat == ImageFormat.RAW_SENSOR) {
+            return SurfaceConfig.ConfigType.RAW;
+        } else {
+            return SurfaceConfig.ConfigType.PRIV;
+        }
+    }
+
     @Nullable
     private Size getTargetSize(@NonNull ImageOutputConfig imageOutputConfig) {
         int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index 1094a2b..ef67bb38 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -44,8 +44,8 @@
         if (ImageCapturePixelHDRPlusQuirk.load()) {
             quirks.add(new ImageCapturePixelHDRPlusQuirk());
         }
-        if (SamsungPreviewTargetAspectRatioQuirk.load()) {
-            quirks.add(new SamsungPreviewTargetAspectRatioQuirk());
+        if (SelectResolutionQuirk.load()) {
+            quirks.add(new SelectResolutionQuirk());
         }
         if (Nexus4AndroidLTargetAspectRatioQuirk.load()) {
             quirks.add(new Nexus4AndroidLTargetAspectRatioQuirk());
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
deleted file mode 100644
index 2d789fb..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.PreviewConfig;
-import androidx.camera.core.impl.Quirk;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Quirk that produces stretched preview on certain Samsung devices.
- *
- * <p> On certain Samsung devices, the HAL provides 16:9 preview even when the Surface size is
- * set to 4:3, which causes the preview to be stretched in PreviewView.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class SamsungPreviewTargetAspectRatioQuirk implements Quirk {
-
-    // List of devices with the issue.
-    private static final List<String> DEVICE_MODELS = Arrays.asList(
-            "SM-J710MN", // b/170762209
-            "SM-T580" // b/169471824
-    );
-
-    static boolean load() {
-        return "SAMSUNG".equalsIgnoreCase(Build.BRAND)
-                && DEVICE_MODELS.contains(android.os.Build.MODEL.toUpperCase(Locale.US));
-    }
-
-    /**
-     * Whether to overwrite the aspect ratio in the config to be 16:9.
-     */
-    public boolean require16_9(@NonNull Config config) {
-        return config instanceof PreviewConfig;
-    }
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SelectResolutionQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SelectResolutionQuirk.java
new file mode 100644
index 0000000..07e7672
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SelectResolutionQuirk.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.SurfaceConfig;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Quirk that requires specific resolutions as the workaround.
+ *
+ * <p> This is an allowlist that selects specific resolutions to override the app provided
+ * resolutions. The resolution provided in this file have been manually tested by CameraX team.
+ */
+@RequiresApi(21)
+public class SelectResolutionQuirk implements Quirk {
+
+    private static final List<String> SAMSUNG_DISTORTION_MODELS = Arrays.asList(
+            "SM-T580", // Samsung Galaxy Tab A (2016)
+            "SM-J710MN", // Samsung Galaxy J7 (2016)
+            "SM-A320FL", // Samsung Galaxy A3 (2017)
+            "SM-G570M"); // Samsung Galaxy J5 Prime
+
+    static boolean load() {
+        return isSamsungDistortion();
+    }
+
+    /**
+     * Selects a resolution based on {@link SurfaceConfig.ConfigType}.
+     *
+     * <p> The selected resolution have been manually tested by CameraX team. It is known to
+     * work for the given device/stream.
+     *
+     * @return null if no resolution provided, in which case the calling code should fallback to
+     * user provided target resolution.
+     */
+    @Nullable
+    public Size selectResolution(@NonNull SurfaceConfig.ConfigType configType) {
+        if (isSamsungDistortion()) {
+            // The following resolutions are needed for both the front and the back camera.
+            switch (configType) {
+                case PRIV:
+                    return new Size(1920, 1080);
+                case YUV:
+                    return new Size(1280, 720);
+                case JPEG:
+                    return new Size(3264, 1836);
+                default:
+                    return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Checks for device model with Samsung output distortion bug (b/190203334).
+     *
+     * <p> The symptom of these devices is that the output of one or many streams, including PRIV,
+     * JPEG and/or YUV, can have an extra 25% crop, and the cropped image is stretched to
+     * fill the Surface, which results in a distorted output. The streams can also have an
+     * extra 25% double crop, in which case the stretched image will not be distorted, but the
+     * FOV is smaller than it should be.
+     *
+     * <p> The behavior is inconsistent in a way that the extra cropping depends on the
+     * resolution of the streams. The existence of the issue also depends on API level and/or
+     * build number. See discussion in go/samsung-camera-distortion.
+     */
+    private static boolean isSamsungDistortion() {
+        return "samsung".equalsIgnoreCase(Build.BRAND)
+                && SAMSUNG_DISTORTION_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+    }
+
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSize.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSize.java
new file mode 100644
index 0000000..6c4da09
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSize.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.workaround;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk;
+import androidx.camera.core.impl.SurfaceConfig;
+
+/**
+ * Helper class that overrides the maximum preview size used in surface combination check.
+ *
+ * @see androidx.camera.camera2.internal.SupportedSurfaceCombination
+ */
+@RequiresApi(21)
+public class MaxPreviewSize {
+
+    @Nullable
+    private final SelectResolutionQuirk mSelectResolutionQuirk;
+
+    /**
+     * Constructs new {@link MaxPreviewSize}.
+     */
+    public MaxPreviewSize() {
+        this(DeviceQuirks.get(SelectResolutionQuirk.class));
+    }
+
+    /**
+     * Constructs new {@link MaxPreviewSize}.
+     */
+    @VisibleForTesting
+    MaxPreviewSize(@Nullable SelectResolutionQuirk selectResolutionQuirk) {
+        mSelectResolutionQuirk = selectResolutionQuirk;
+    }
+
+    /**
+     * Gets the max preview resolution based on the default preview max resolution.
+     *
+     * <p> If select resolution is larger than the default resolution, return the select
+     * resolution. The select resolution has been manually tested on the device. Otherwise,
+     * return the default max resolution.
+     */
+    @NonNull
+    public Size getMaxPreviewResolution(@NonNull Size defaultMaxPreviewResolution) {
+        if (mSelectResolutionQuirk == null) {
+            return defaultMaxPreviewResolution;
+        }
+        Size selectResolution = mSelectResolutionQuirk.selectResolution(
+                SurfaceConfig.ConfigType.PRIV);
+        if (selectResolution == null) {
+            return defaultMaxPreviewResolution;
+        }
+        boolean isSelectResolutionLarger =
+                selectResolution.getWidth() * selectResolution.getHeight()
+                        > defaultMaxPreviewResolution.getWidth()
+                        * defaultMaxPreviewResolution.getHeight();
+        return isSelectResolutionLarger ? selectResolution : defaultMaxPreviewResolution;
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelector.java
new file mode 100644
index 0000000..c463458
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelector.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.workaround;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk;
+import androidx.camera.core.impl.SurfaceConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class that overrides user configured resolution with resolution selected based on device
+ * quirks.
+ */
+@RequiresApi(21)
+public class ResolutionSelector {
+
+    @Nullable
+    private final SelectResolutionQuirk mSelectResolutionQuirk;
+
+    /**
+     * Constructs new {@link ResolutionSelector}.
+     */
+    public ResolutionSelector() {
+        this(DeviceQuirks.get(SelectResolutionQuirk.class));
+    }
+
+    /**
+     * Constructs new {@link ResolutionSelector}.
+     */
+    @VisibleForTesting
+    ResolutionSelector(@Nullable SelectResolutionQuirk selectResolutionQuirk) {
+        mSelectResolutionQuirk = selectResolutionQuirk;
+    }
+
+    /**
+     * Returns a new list of resolution with the selected resolution inserted or prioritized.
+     *
+     * <p> If the list contains the selected resolution, move it to be the first element; if it
+     * does not contain the selected resolution, insert it as the first element; if there is no
+     * device quirk, return the original list.
+     *
+     * @param configType           the config type based on which the supported resolution is
+     *                             calculated.
+     * @param supportedResolutions a ordered list of resolutions calculated by CameraX.
+     */
+    @NonNull
+    public List<Size> insertOrPrioritize(
+            @NonNull SurfaceConfig.ConfigType configType,
+            @NonNull List<Size> supportedResolutions) {
+        if (mSelectResolutionQuirk == null) {
+            return supportedResolutions;
+        }
+        Size selectResolution = mSelectResolutionQuirk.selectResolution(configType);
+        if (selectResolution == null) {
+            return supportedResolutions;
+        }
+        List<Size> newResolutions = new ArrayList<>();
+        newResolutions.add(selectResolution);
+        for (Size size : supportedResolutions) {
+            if (!size.equals(selectResolution)) {
+                newResolutions.add(size);
+            }
+        }
+        return newResolutions;
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java
index ca81eca..667c0f1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatio.java
@@ -25,8 +25,6 @@
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.camera2.internal.compat.quirk.Nexus4AndroidLTargetAspectRatioQuirk;
-import androidx.camera.camera2.internal.compat.quirk.SamsungPreviewTargetAspectRatioQuirk;
-import androidx.camera.core.impl.ImageOutputConfig;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -34,7 +32,6 @@
 /**
  * Workaround to get corrected target aspect ratio.
  *
- * @see SamsungPreviewTargetAspectRatioQuirk
  * @see Nexus4AndroidLTargetAspectRatioQuirk
  * @see AspectRatioLegacyApi21Quirk
  */
@@ -53,13 +50,8 @@
      * Gets corrected target aspect ratio based on device and camera quirks.
      */
     @TargetAspectRatio.Ratio
-    public int get(@NonNull ImageOutputConfig imageOutputConfig, @NonNull String cameraId,
+    public int get(@NonNull String cameraId,
             @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
-        final SamsungPreviewTargetAspectRatioQuirk samsungQuirk =
-                DeviceQuirks.get(SamsungPreviewTargetAspectRatioQuirk.class);
-        if (samsungQuirk != null && samsungQuirk.require16_9(imageOutputConfig)) {
-            return TargetAspectRatio.RATIO_16_9;
-        }
         final Nexus4AndroidLTargetAspectRatioQuirk nexus4AndroidLQuirk =
                 DeviceQuirks.get(Nexus4AndroidLTargetAspectRatioQuirk.class);
         if (nexus4AndroidLQuirk != null) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
index 5b6c39c..3d2d0e9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraControl.java
@@ -215,15 +215,15 @@
     }
 
     /**
-     * Clears all capture request options.
+     * Clears all capture request options that is currently applied by the
+     * {@link Camera2CameraControl}.
      *
      * @return a {@link ListenableFuture} which completes when the repeating
      * {@link android.hardware.camera2.CaptureResult} shows the options have be submitted
      * completely. The future fails with {@link CameraControl.OperationCanceledException} if newer
      * options are set or camera is closed before the current request completes.
-     * @hide
      */
-    @RestrictTo(Scope.LIBRARY)
+    @SuppressWarnings("AsyncSuffixFuture")
     @NonNull
     public ListenableFuture<Void> clearCaptureRequestOptions() {
         clearCaptureRequestOptionsInternal();
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionSelectorQuirkTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionSelectorQuirkTest.java
new file mode 100644
index 0000000..23a748d
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/quirk/ResolutionSelectorQuirkTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.quirk;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.util.ReflectionHelpers;
+
+/**
+ * Unit tests for {@link SelectResolutionQuirk}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ResolutionSelectorQuirkTest {
+
+
+    @Test
+    public void samsungDistortionHasQuirks() {
+        ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
+        // Default is false
+        assertThat(SelectResolutionQuirk.load()).isFalse();
+
+        // Test all samsung models
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-T580");
+        assertThat(SelectResolutionQuirk.load()).isTrue();
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-J710MN");
+        assertThat(SelectResolutionQuirk.load()).isTrue();
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-A320FL");
+        assertThat(SelectResolutionQuirk.load()).isTrue();
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G570M");
+        assertThat(SelectResolutionQuirk.load()).isTrue();
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSizeTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSizeTest.kt
new file mode 100644
index 0000000..2886415d
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/MaxPreviewSizeTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val SELECT_RESOLUTION_PRIV = Size(1001, 1000)
+private val SELECT_RESOLUTION_YUV = Size(1002, 1000)
+private val SELECT_RESOLUTION_JPEG = Size(1003, 1000)
+
+/**
+ * Unit tests for [MaxPreviewSize].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class MaxPreviewSizeTest {
+
+    private val mMaxPreviewSize = MaxPreviewSize(object : SelectResolutionQuirk() {
+        override fun selectResolution(configType: SurfaceConfig.ConfigType): Size? {
+            return when (configType) {
+                SurfaceConfig.ConfigType.YUV -> SELECT_RESOLUTION_YUV
+                SurfaceConfig.ConfigType.PRIV -> SELECT_RESOLUTION_PRIV
+                SurfaceConfig.ConfigType.JPEG -> SELECT_RESOLUTION_JPEG
+                else -> null
+            }
+        }
+    })
+
+    @Test
+    fun largerThanDefaultPreviewResolution_returnsPrivResolution() {
+        val smallSize = Size(101, 100)
+        assertThat(mMaxPreviewSize.getMaxPreviewResolution(smallSize)).isEqualTo(
+            SELECT_RESOLUTION_PRIV
+        )
+    }
+
+    @Test
+    fun smallerThanDefaultResolution_returnsDefaultResolution() {
+        val largeDefaultSize = Size(10001, 10000)
+        assertThat(mMaxPreviewSize.getMaxPreviewResolution(largeDefaultSize)).isEqualTo(
+            largeDefaultSize
+        )
+    }
+
+    @Test
+    fun noQuirk_returnsOriginalMaxPreviewResolution() {
+        noQuirk_returnsOriginalMaxPreviewResolution(null)
+    }
+
+    @Test
+    fun noResolutionQuirk_returnsOriginalMaxPreviewResolution() {
+        noQuirk_returnsOriginalMaxPreviewResolution(
+            Mockito.mock(
+                SelectResolutionQuirk::class.java
+            )
+        )
+    }
+
+    private fun noQuirk_returnsOriginalMaxPreviewResolution(
+        quirk: SelectResolutionQuirk?
+    ) {
+        val maxPreviewSize = MaxPreviewSize(quirk)
+        val result =
+            maxPreviewSize.getMaxPreviewResolution(SELECT_RESOLUTION_JPEG)
+        assertThat(result).isEqualTo(SELECT_RESOLUTION_JPEG)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelectorTest.kt
new file mode 100644
index 0000000..f95f2c3
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ResolutionSelectorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.camera.camera2.internal.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.internal.compat.quirk.SelectResolutionQuirk
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val RESOLUTION_1 = Size(101, 100)
+private val RESOLUTION_2 = Size(102, 100)
+
+private val SELECT_RESOLUTION_PRIV = Size(1001, 1000)
+private val SELECT_RESOLUTION_YUV = Size(1002, 1000)
+private val SELECT_RESOLUTION_JPEG = Size(1003, 1000)
+
+private val SUPPORTED_RESOLUTIONS = listOf(RESOLUTION_1, RESOLUTION_2)
+
+/**
+ * Unit test for [ResolutionSelector].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionSelectorTest {
+
+    private val mResolutionSelector = ResolutionSelector(object : SelectResolutionQuirk() {
+        override fun selectResolution(configType: SurfaceConfig.ConfigType): Size? {
+            return when (configType) {
+                SurfaceConfig.ConfigType.YUV -> SELECT_RESOLUTION_YUV
+                SurfaceConfig.ConfigType.PRIV -> SELECT_RESOLUTION_PRIV
+                SurfaceConfig.ConfigType.JPEG -> SELECT_RESOLUTION_JPEG
+                else -> null
+            }
+        }
+    })
+
+    @Test
+    fun hasPrivResolution_prioritized() {
+        hasResolution_prioritized(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+    }
+
+    @Test
+    fun hasYuvResolution_prioritized() {
+        hasResolution_prioritized(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+    }
+
+    @Test
+    fun hasJpegResolution_prioritized() {
+        hasResolution_prioritized(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+    }
+
+    private fun hasResolution_prioritized(
+        configType: SurfaceConfig.ConfigType,
+        resolution: Size
+    ) {
+        val resolutions: MutableList<Size> = ArrayList<Size>(SUPPORTED_RESOLUTIONS)
+        resolutions.add(resolution)
+        Truth.assertThat(mResolutionSelector.insertOrPrioritize(configType, resolutions))
+            .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+    }
+
+    @Test
+    fun noPrivResolution_inserted() {
+        noResolution_inserted(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+    }
+
+    @Test
+    fun noYuvResolution_inserted() {
+        noResolution_inserted(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+    }
+
+    @Test
+    fun noJpegResolution_inserted() {
+        noResolution_inserted(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+    }
+
+    private fun noResolution_inserted(
+        configType: SurfaceConfig.ConfigType,
+        resolution: Size
+    ) {
+        Truth.assertThat(mResolutionSelector.insertOrPrioritize(configType, SUPPORTED_RESOLUTIONS))
+            .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+    }
+
+    @Test
+    fun noQuirk_returnsOriginalSupportedResolutions() {
+        noQuirk_returnsOriginalSupportedResolutions(null)
+    }
+
+    @Test
+    fun noResolution_returnsOriginalSupportedResolutions() {
+        noQuirk_returnsOriginalSupportedResolutions(getEmptyQuirk())
+    }
+
+    private fun noQuirk_returnsOriginalSupportedResolutions(
+        quirk: SelectResolutionQuirk?
+    ) {
+        val resolutionSelector = ResolutionSelector(quirk)
+        val result = resolutionSelector.insertOrPrioritize(
+            SurfaceConfig.ConfigType.PRIV,
+            SUPPORTED_RESOLUTIONS
+        )
+        Truth.assertThat(result).containsExactlyElementsIn(SUPPORTED_RESOLUTIONS)
+    }
+
+    private fun getEmptyQuirk(): SelectResolutionQuirk? {
+        return Mockito.mock(SelectResolutionQuirk::class.java)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
index 8049c3b..b5a3aac4 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
@@ -38,7 +38,6 @@
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.Preview;
 import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.ImageOutputConfig;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -66,24 +65,6 @@
     @ParameterizedRobolectricTestRunner.Parameters
     public static Collection<Object[]> data() {
         final List<Object[]> data = new ArrayList<>();
-        data.add(new Object[]{new Config("Samsung", "SM-J710MN", true,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_16_9, ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-J710MN", true,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_16_9, ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-J710MN", false,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_ORIGINAL, ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-J710MN", false,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_ORIGINAL,
-                ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-T580", true,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_16_9, ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-T580", true,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_16_9, ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-T580", false,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_ORIGINAL, ALL_API_LEVELS)});
-        data.add(new Object[]{new Config("Samsung", "SM-T580", false,
-                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_ORIGINAL,
-                ALL_API_LEVELS)});
         data.add(new Object[]{new Config("Google", "Nexus 4", true,
                 INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, RATIO_4_3, RATIO_MAX_JPEG,
                 new Range<>(21, 22))});
@@ -146,9 +127,7 @@
                     .setTargetAspectRatio(mConfig.mInputAspectRatio)
                     .build();
         }
-        final ImageOutputConfig imageOutputConfig = (ImageOutputConfig) usecase.getCurrentConfig();
-
-        final int aspectRatio = new TargetAspectRatio().get(imageOutputConfig,
+        final int aspectRatio = new TargetAspectRatio().get(
                 BACK_CAMERA_ID, getCharacteristicsCompat(mConfig.mHardwareLevel));
         assertThat(aspectRatio).isEqualTo(getExpectedAspectRatio());
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 8f2ccf3..b82e649 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -883,7 +883,9 @@
          * output stream under 1080p.
          *
          * <p>If not set, the default selected resolution will be the best size match to the
-         * device's screen resolution, or to 1080p (1920x1080), whichever is smaller.
+         * device's screen resolution, or to 1080p (1920x1080), whichever is smaller. Note that
+         * due to compatibility reasons, CameraX may select a resolution that is larger than the
+         * default screen resolution on certain devices.
          *
          * <p>When using the <code>camera-camera2</code> CameraX implementation, which resolution
          * will be finally selected will depend on the camera device's hardware level and the
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java b/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java
index c799dad..a5ab258 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/LoggerTest.java
@@ -25,7 +25,6 @@
 import androidx.annotation.Nullable;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.ParameterizedRobolectricTestRunner;
@@ -136,7 +135,6 @@
                 .hasNoMoreMessages();
     }
 
-    @Ignore("b/208252595")
     @Config(maxSdk = 23)
     @Test
     public void log_truncateLongTag() {
@@ -232,15 +230,51 @@
         LogAssert hasMessage(int priority, String tag, String message,
                 @Nullable Throwable throwable) {
             final LogItem item = mLogItems.get(mIndex++);
-            assertThat(item.type).isEqualTo(priority);
-            assertThat(item.tag).isEqualTo(tag);
-            assertThat(item.msg).isEqualTo(message);
-            assertThat(item.throwable).isEqualTo(throwable);
+
+            try {
+                assertThat(item.type).isEqualTo(priority);
+                assertThat(item.tag).isEqualTo(tag);
+                assertThat(item.msg).isEqualTo(message);
+                assertThat(item.throwable).isEqualTo(throwable);
+            } catch (Throwable e) {
+                // TODO(b/208252595): Dump the log info for the issue clarification.
+                throw new RuntimeException(collectLogItemsInfo() + "\n" + e);
+            }
             return this;
         }
 
         void hasNoMoreMessages() {
-            assertThat(mIndex).isEqualTo(mLogItems.size());
+            try {
+                assertThat(mIndex).isEqualTo(mLogItems.size());
+            } catch (Throwable e) {
+                // TODO(b/207674161): Dump the log info for the issue clarification.
+                throw new RuntimeException(collectLogItemsInfo() + "\n" + e);
+            }
+        }
+
+        private String collectLogItemsInfo() {
+            int counter = 0;
+            String logItemsInfo =
+                    "Log items count is " + mLogItems.size() + ", mIndex is " + mIndex + "\n";
+
+            for (int i = mIndex; i >= 0; i--) {
+                if (i >= mLogItems.size()) {
+                    continue;
+                }
+
+                LogItem item = mLogItems.get(i);
+                logItemsInfo +=
+                        "index: " + i + ", item.type: " + item.type + ", item.tag: " + item.tag
+                                + ", item.msg: " + item.msg + ", item.throwable: "
+                                + item.throwable + "\n";
+
+                // Prints five items at most in case too many items exist to cause problem.
+                if (counter++ > 5) {
+                    break;
+                }
+            }
+
+            return logItemsInfo;
         }
     }
 }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index e587fd9..9200dae7 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -52,7 +52,6 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.ViewPort;
 import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
 import androidx.camera.view.internal.compat.quirk.TextureViewRotationQuirk;
 import androidx.core.util.Preconditions;
 
@@ -103,12 +102,9 @@
 
     // SurfaceRequest.getResolution().
     private Size mResolution;
-    // This represents the area of the Surface that should be visible to end users. The value
-    // is based on TransformationInfo.getCropRect() with possible corrections due to device quirks.
+    // This represents the area of the Surface that should be visible to end users. The area is
+    // defined by the Viewport class.
     private Rect mSurfaceCropRect;
-    // This rect represents the size of the viewport in preview. It's always the same as
-    // TransformationInfo.getCropRect().
-    private Rect mViewportRect;
     // TransformationInfo.getRotationDegrees().
     private int mPreviewRotationDegrees;
     // TransformationInfo.getTargetRotation.
@@ -132,8 +128,7 @@
             Size resolution, boolean isFrontCamera) {
         Logger.d(TAG, "Transformation info set: " + transformationInfo + " " + resolution + " "
                 + isFrontCamera);
-        mSurfaceCropRect = getCorrectedCropRect(transformationInfo.getCropRect());
-        mViewportRect = transformationInfo.getCropRect();
+        mSurfaceCropRect = transformationInfo.getCropRect();
         mPreviewRotationDegrees = transformationInfo.getRotationDegrees();
         mTargetRotation = transformationInfo.getTargetRotation();
         mResolution = resolution;
@@ -279,28 +274,6 @@
     }
 
     /**
-     * Gets the vertices of the crop rect in Surface.
-     */
-    private Rect getCorrectedCropRect(Rect surfaceCropRect) {
-        PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
-        if (quirk != null) {
-            // Correct crop rect if the device has a quirk.
-            RectF cropRectF = new RectF(surfaceCropRect);
-            Matrix correction = new Matrix();
-            correction.setScale(
-                    quirk.getCropRectScaleX(),
-                    1f,
-                    surfaceCropRect.centerX(),
-                    surfaceCropRect.centerY());
-            correction.mapRect(cropRectF);
-            Rect correctRect = new Rect();
-            cropRectF.round(correctRect);
-            return correctRect;
-        }
-        return surfaceCropRect;
-    }
-
-    /**
      * Gets the viewport rect in {@link PreviewView} coordinates for the case where viewport's
      * aspect ratio doesn't match {@link PreviewView}'s aspect ratio.
      *
@@ -380,9 +353,9 @@
      */
     private Size getRotatedViewportSize() {
         if (is90or270(mPreviewRotationDegrees)) {
-            return new Size(mViewportRect.height(), mViewportRect.width());
+            return new Size(mSurfaceCropRect.height(), mSurfaceCropRect.width());
         }
-        return new Size(mViewportRect.width(), mViewportRect.height());
+        return new Size(mSurfaceCropRect.width(), mSurfaceCropRect.height());
     }
 
     /**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index f4cfba3..61b8959 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -40,11 +40,6 @@
     static List<Quirk> loadQuirks() {
         final List<Quirk> quirks = new ArrayList<>();
 
-        // Load all device specific quirks
-        if (PreviewOneThirdWiderQuirk.load()) {
-            quirks.add(new PreviewOneThirdWiderQuirk());
-        }
-
         if (SurfaceViewStretchedQuirk.load()) {
             quirks.add(new SurfaceViewStretchedQuirk());
         }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
deleted file mode 100644
index fb8efa1d..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.camera.view.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.Quirk;
-
-/**
- * A quirk where the preview buffer is stretched.
- *
- * <p> The symptom is, the preview's FOV is always 1/3 wider than intended. For example, if the
- * preview Surface is 800x600, it's actually has a FOV of 1066x600 with the same center point,
- * but squeezed to fit the 800x600 buffer.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class PreviewOneThirdWiderQuirk implements Quirk {
-
-    private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
-    private static final String SAMSUNG_J5_PRIME = "ON5XELTE"; // b/183329599
-
-    static boolean load() {
-        boolean isSamsungJ5PrimeAndApi26 =
-                SAMSUNG_J5_PRIME.equals(Build.DEVICE.toUpperCase()) && Build.VERSION.SDK_INT >= 26;
-        boolean isSamsungA3 = SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase());
-        return isSamsungJ5PrimeAndApi26 || isSamsungA3;
-    }
-
-    /**
-     * The mount that the crop rect needs to be scaled in x.
-     */
-    public float getCropRectScaleX() {
-        return 0.75f;
-    }
-}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
index bc3c951..b225d17 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
@@ -25,8 +25,6 @@
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue
 import androidx.camera.view.TransformUtils.sizeToVertices
-import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk
-import androidx.camera.view.internal.compat.quirk.QuirkInjector
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -84,23 +82,6 @@
     }
 
     @Test
-    fun withPreviewStretchedQuirk_cropRectIsAdjusted() {
-        // Arrange.
-        QuirkInjector.inject(PreviewOneThirdWiderQuirk())
-
-        // Act.
-        mPreviewTransform.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of(FULL_CROP_RECT, 0, 0),
-            Size(FULL_CROP_RECT.width(), FULL_CROP_RECT.height()),
-            /*isFrontCamera=*/false
-        )
-
-        // Assert: the crop rect is corrected.
-        assertThat(mPreviewTransform.surfaceCropRect).isEqualTo(Rect(8, 0, 53, 40))
-        QuirkInjector.clear()
-    }
-
-    @Test
     fun cropRectWidthOffByOnePixel_match() {
         assertThat(
             isCropRectAspectRatioMatchPreviewView(
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
deleted file mode 100644
index 17c02f5..0000000
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.camera.view.internal.compat.quirk;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Build;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.internal.DoNotInstrument;
-import org.robolectric.util.ReflectionHelpers;
-
-/**
- * Unit tests for {@link PreviewOneThirdWiderQuirk}.
- */
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewOneThirdWiderQuirkTest {
-
-    @Test
-    public void quirkExistsOnSamsungA3() {
-        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
-        assertPreviewShouldBeCroppedBy25Percent();
-    }
-
-    @Test
-    @Config(minSdk = Build.VERSION_CODES.O)
-    public void quirkExistsOnSamsungJ5PrimeApi26AndAbove() {
-        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
-        assertPreviewShouldBeCroppedBy25Percent();
-    }
-
-    @Test
-    @Config(maxSdk = Build.VERSION_CODES.N_MR1)
-    public void quirkDoesNotExistOnSamsungJ5PrimeApi25AndBelow() {
-        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
-        assertThat(DeviceQuirks.get(PreviewOneThirdWiderQuirk.class)).isNull();
-    }
-
-    private void assertPreviewShouldBeCroppedBy25Percent() {
-        final PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
-        assertThat(quirk).isNotNull();
-        assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
-    }
-}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
index 5db919d..6192e78 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -26,11 +26,16 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
 import android.graphics.Insets;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.util.Log;
+import android.view.PixelCopy;
 import android.view.View;
 import android.view.WindowInsets;
+import android.widget.ImageView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -111,6 +116,16 @@
     LoadingView mLoadingView;
     View mActivityContainerView;
     View mLocalContentContainerView;
+
+    /** Displays the snapshot of the surface view to avoid a visual glitch when app comes
+     * to foreground. This view sits behind the surface view and will be visible only when surface
+     * is hidden (or not created yet).
+     */
+    ImageView mSurfaceSnapshotView;
+
+    // The handler used to take surface view snapshot.
+    private Handler mSnapshotHandler = new Handler(Looper.myLooper());
+
     @Nullable SurfaceHolderListener mSurfaceHolderListener;
     @Nullable ActivityLifecycleDelegate mActivityLifecycleDelegate;
     @Nullable CarAppViewModel mViewModel;
@@ -250,6 +265,7 @@
         mSurfaceView = requireViewById(R.id.template_view_surface);
         mErrorMessageView = requireViewById(R.id.error_message_view);
         mLoadingView = requireViewById(R.id.loading_view);
+        mSurfaceSnapshotView = requireViewById(R.id.template_view_snapshot);
 
         mActivityContainerView.setOnApplyWindowInsetsListener(mWindowInsetsListener);
         // IMPORTANT: The SystemUiVisibility applied here must match the insets provided to the
@@ -291,6 +307,30 @@
         mViewModel.bind(getIntent(), mCarActivity, getDisplayId());
     }
 
+    /** Takes a snapshot of the surface view and puts it in the surfaceSnapshotView if succeeded. */
+    private void takeSurfaceSnapshot() {
+        // Nothing to do if the surface is not ready yet.
+        if (mSurfaceView.getHolder().getSurface() == null) {
+            return;
+        }
+        Bitmap bitmap = Bitmap.createBitmap(mSurfaceView.getWidth(), mSurfaceView.getHeight(),
+                Bitmap.Config.ARGB_8888);
+        PixelCopy.request(mSurfaceView, bitmap, status -> {
+            if (status == PixelCopy.SUCCESS) {
+                mSurfaceSnapshotView.setImageBitmap(bitmap);
+            } else {
+                Log.w(LogTags.TAG, "Failed to take snapshot of the surface view");
+                mSurfaceSnapshotView.setImageBitmap(null);
+            }
+        }, mSnapshotHandler);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        takeSurfaceSnapshot();
+    }
+
     // TODO(b/189862860): Address SOFT_INPUT_ADJUST_RESIZE deprecation
     @SuppressWarnings("deprecation")
     private void setSoftInputHandling() {
@@ -313,28 +353,33 @@
     private void onStateChanged(@NonNull CarAppViewModel.State state) {
         ThreadUtils.runOnMain(() -> {
             requireNonNull(mSurfaceView);
+            requireNonNull(mSurfaceSnapshotView);
             requireNonNull(mSurfaceHolderListener);
 
             switch (state) {
                 case IDLE:
                     mSurfaceView.setVisibility(View.GONE);
+                    mSurfaceSnapshotView.setVisibility(View.VISIBLE);
                     mSurfaceHolderListener.setSurfaceListener(null);
                     mErrorMessageView.setVisibility(View.GONE);
                     mLoadingView.setVisibility(View.GONE);
                     break;
                 case ERROR:
                     mSurfaceView.setVisibility(View.GONE);
+                    mSurfaceSnapshotView.setVisibility(View.GONE);
                     mSurfaceHolderListener.setSurfaceListener(null);
                     mErrorMessageView.setVisibility(View.VISIBLE);
                     mLoadingView.setVisibility(View.GONE);
                     break;
                 case CONNECTING:
                     mSurfaceView.setVisibility(View.GONE);
+                    mSurfaceSnapshotView.setVisibility(View.VISIBLE);
                     mErrorMessageView.setVisibility(View.GONE);
                     mLoadingView.setVisibility(View.VISIBLE);
                     break;
                 case CONNECTED:
                     mSurfaceView.setVisibility(View.VISIBLE);
+                    mSurfaceSnapshotView.setVisibility(View.VISIBLE);
                     mErrorMessageView.setVisibility(View.GONE);
                     mLoadingView.setVisibility(View.GONE);
                     break;
diff --git a/car/app/app-automotive/src/main/res/layout/activity_template.xml b/car/app/app-automotive/src/main/res/layout/activity_template.xml
index 5ee02db..221dcd3 100644
--- a/car/app/app-automotive/src/main/res/layout/activity_template.xml
+++ b/car/app/app-automotive/src/main/res/layout/activity_template.xml
@@ -5,6 +5,13 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
+    <!-- Used to display the snapshot of the surface view to avoid a visual glitch when app comes
+         to foreground. -->
+    <ImageView
+        android:id="@+id/template_view_snapshot"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
     <androidx.car.app.activity.renderer.surface.TemplateSurfaceView
         android:id="@+id/template_view_surface"
         android:layout_width="match_parent"
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java
index cc12d54..84cf7ad 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/RoutePreviewDemoScreen.java
@@ -24,6 +24,7 @@
 import androidx.car.app.CarContext;
 import androidx.car.app.CarToast;
 import androidx.car.app.Screen;
+import androidx.car.app.constraints.ConstraintManager;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.CarText;
 import androidx.car.app.model.DurationSpan;
@@ -32,6 +33,7 @@
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
 import androidx.car.app.sample.showcase.common.navigation.routing.RoutingDemoModels;
+import androidx.car.app.versioning.CarAppApiLevels;
 
 import java.util.concurrent.TimeUnit;
 
@@ -41,23 +43,65 @@
         super(carContext);
     }
 
+    private CarText createRouteText(int index) {
+        switch (index) {
+            case 0:
+                // Set text variants for the first route.
+                SpannableString shortRouteLongText = new SpannableString(
+                        "   \u00b7 ---------------- Short" + "  " + "route "
+                                + "-------------------");
+                shortRouteLongText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1,
+                        0);
+                SpannableString firstRouteShortText = new SpannableString("   \u00b7 Short route");
+                firstRouteShortText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1,
+                        0);
+                return new CarText.Builder(shortRouteLongText)
+                        .addVariant(firstRouteShortText)
+                        .build();
+            case 1:
+                SpannableString lessBusyRouteText = new SpannableString("   \u00b7 Less busy");
+                lessBusyRouteText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(24)), 0, 1,
+                        0);
+                return new CarText.Builder(lessBusyRouteText).build();
+            case 2:
+                SpannableString hovRouteText = new SpannableString("   \u00b7 HOV friendly");
+                hovRouteText.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867)), 0, 1, 0);
+                return new CarText.Builder(hovRouteText).build();
+            default:
+                SpannableString routeText = new SpannableString("   \u00b7 Long route");
+                routeText.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867L + index)),
+                        0, 1, 0);
+                return new CarText.Builder(routeText).build();
+        }
+    }
+
+    private Row createRow(int index) {
+        CarText route = createRouteText(index);
+        String titleText = "Via NE " + (index + 4) + "th Street";
+
+        return new Row.Builder()
+                .setTitle(route)
+                .addText(titleText)
+                .build();
+    }
+
     @NonNull
     @Override
     public Template onGetTemplate() {
-        // Set text variants for the first route.
-        SpannableString firstRouteLongText = new SpannableString(
-                "   \u00b7 ---------------- Short" + "  " + "route " + "-------------------");
-        firstRouteLongText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
-        SpannableString firstRouteShortText = new SpannableString("   \u00b7 Short Route");
-        firstRouteShortText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
-        CarText firstRoute = new CarText.Builder(firstRouteLongText)
-                .addVariant(firstRouteShortText)
-                .build();
+        int itemLimit = 3;
+        // Adjust the item limit according to the car constrains.
+        if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
+            itemLimit = getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+                    ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST);
+        }
 
-        SpannableString secondRoute = new SpannableString("   \u00b7 Less busy");
-        secondRoute.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(24)), 0, 1, 0);
-        SpannableString thirdRoute = new SpannableString("   \u00b7 HOV friendly");
-        thirdRoute.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867)), 0, 1, 0);
+        ItemList.Builder itemListBuilder = new ItemList.Builder()
+                .setOnSelectedListener(this::onRouteSelected)
+                .setOnItemsVisibilityChangedListener(this::onRoutesVisible);
+
+        for (int i = 0; i < itemLimit; i++) {
+            itemListBuilder.addItem(createRow(i));
+        }
 
         // Set text variants for the navigate action text.
         CarText navigateActionText =
@@ -65,26 +109,7 @@
                         + "route").build();
 
         return new RoutePreviewNavigationTemplate.Builder()
-                .setItemList(
-                        new ItemList.Builder()
-                                .setOnSelectedListener(this::onRouteSelected)
-                                .addItem(
-                                        new Row.Builder()
-                                                .setTitle(firstRoute)
-                                                .addText("Via NE 8th Street")
-                                                .build())
-                                .addItem(
-                                        new Row.Builder()
-                                                .setTitle(secondRoute)
-                                                .addText("Via NE 1st Ave")
-                                                .build())
-                                .addItem(
-                                        new Row.Builder()
-                                                .setTitle(thirdRoute)
-                                                .addText("Via NE 4th Street")
-                                                .build())
-                                .setOnItemsVisibilityChangedListener(this::onRoutesVisible)
-                                .build())
+                .setItemList(itemListBuilder.build())
                 .setNavigateAction(
                         new Action.Builder()
                                 .setTitle(navigateActionText)
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java
index 9767ef1..86fb82a 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/GridTemplateDemoScreen.java
@@ -16,7 +16,7 @@
 
 package androidx.car.app.sample.showcase.common.templates;
 
-import static androidx.car.app.CarToast.LENGTH_LONG;
+import static androidx.car.app.CarToast.LENGTH_SHORT;
 import static androidx.car.app.model.Action.BACK;
 
 import android.content.res.Resources;
@@ -85,35 +85,30 @@
         triggerFourthItemLoading();
     }
 
-    @NonNull
-    @Override
-    public Template onGetTemplate() {
-        ItemList.Builder gridItemListBuilder = new ItemList.Builder();
-
-        // Grid item with an icon and a title.
-        gridItemListBuilder.addItem(
-                new GridItem.Builder()
+    private GridItem createGridItem(int index) {
+        switch (index) {
+            case 0:
+                // Grid item with an icon and a title.
+                return new GridItem.Builder()
                         .setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
                         .setTitle("Non-actionable")
-                        .build());
-
-        // Grid item with an icon, a title, onClickListener and no text.
-        gridItemListBuilder.addItem(
-                new GridItem.Builder()
+                        .build();
+            case 1:
+                // Grid item with an icon, a title, onClickListener and no text.
+                return new GridItem.Builder()
                         .setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
                         .setTitle("Second Item")
                         .setOnClickListener(
-                                () ->
-                                        CarToast.makeText(
-                                                getCarContext(),
-                                                "Clicked second item",
-                                                LENGTH_LONG)
-                                                .show())
-                        .build());
-
-        // Grid item with an icon marked as icon, a title, a text and a toggle in unchecked state.
-        gridItemListBuilder.addItem(
-                new GridItem.Builder()
+                                () -> CarToast.makeText(
+                                        getCarContext(),
+                                        "Clicked second item",
+                                        LENGTH_SHORT)
+                                        .show())
+                        .build();
+            case 2:
+                // Grid item with an icon marked as icon, a title, a text and a toggle in
+                // unchecked state.
+                return new GridItem.Builder()
                         .setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
                         .setTitle("Third Item")
                         .setText(mThirdItemToggleState ? "Checked" : "Unchecked")
@@ -123,35 +118,33 @@
                                     CarToast.makeText(
                                             getCarContext(),
                                             "Third item checked: " + mThirdItemToggleState,
-                                            LENGTH_LONG)
+                                            LENGTH_SHORT)
                                             .show();
                                     invalidate();
                                 })
-                        .build());
-
-        // Grid item with an image, a title, a long text and a toggle that takes some time to
-        // update.
-        if (mIsFourthItemLoading) {
-            gridItemListBuilder.addItem(
-                    new GridItem.Builder()
+                        .build();
+            case 3:
+                // Grid item with an image, a title, a long text and a toggle that takes some
+                // time to
+                // update.
+                if (mIsFourthItemLoading) {
+                    return new GridItem.Builder()
                             .setTitle("Fourth")
                             .setText(mFourthItemToggleState ? "On" : "Off")
                             .setLoading(true)
-                            .build());
-        } else {
-            gridItemListBuilder.addItem(
-                    new GridItem.Builder()
+                            .build();
+                } else {
+                    return new GridItem.Builder()
                             .setImage(new CarIcon.Builder(mImage).build())
                             .setTitle("Fourth")
                             .setText(mFourthItemToggleState ? "On" : "Off")
-                            .setOnClickListener(
-                                    this::triggerFourthItemLoading)
-                            .build());
-        }
-
-        // Grid item with a large image, a long title, no text and a toggle in unchecked state.
-        gridItemListBuilder.addItem(
-                new GridItem.Builder()
+                            .setOnClickListener(this::triggerFourthItemLoading)
+                            .build();
+                }
+            case 4:
+                // Grid item with a large image, a long title, no text and a toggle in unchecked
+                // state.
+                return new GridItem.Builder()
                         .setImage(new CarIcon.Builder(mImage).build(), GridItem.IMAGE_TYPE_LARGE)
                         .setTitle("Fifth Item has a long title set")
                         .setOnClickListener(
@@ -160,52 +153,62 @@
                                     CarToast.makeText(
                                             getCarContext(),
                                             "Fifth item checked: " + mFifthItemToggleState,
-                                            LENGTH_LONG)
+                                            LENGTH_SHORT)
                                             .show();
                                     invalidate();
                                 })
-                        .build());
-
-        // Grid item with an image marked as an icon, a long title, a long text and onClickListener.
-        gridItemListBuilder.addItem(
-                new GridItem.Builder()
-                        .setImage(new CarIcon.Builder(mIcon).build(), GridItem.IMAGE_TYPE_ICON)
-                        .setTitle("Sixth Item has a long title set")
-                        .setText("Sixth Item has a long text set")
-                        .setOnClickListener(
-                                () ->
-                                        CarToast.makeText(
-                                                getCarContext(),
-                                                "Clicked sixth item",
-                                                LENGTH_LONG)
-                                                .show())
-                        .build());
-
-        // Some hosts may allow more items in the grid than others, so create more.
-        if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
-            int itemLimit =
-                    Math.min(MAX_GRID_ITEMS,
-                            getCarContext().getCarService(ConstraintManager.class).getContentLimit(
-                                    ConstraintManager.CONTENT_LIMIT_TYPE_GRID));
-
-            for (int i = 7; i <= itemLimit; i++) {
-                String titleText = "Item: " + i;
-                String toastText = "Clicked item " + i;
-
-                gridItemListBuilder.addItem(
+                        .build();
+            case 5:
+                // Grid item with an image marked as an icon, a long title, a long text and
+                // onClickListener.
+                return
                         new GridItem.Builder()
                                 .setImage(new CarIcon.Builder(mIcon).build(),
                                         GridItem.IMAGE_TYPE_ICON)
-                                .setTitle(titleText)
+                                .setTitle("Sixth Item has a long title set")
+                                .setText("Sixth Item has a long text set")
                                 .setOnClickListener(
                                         () ->
                                                 CarToast.makeText(
                                                         getCarContext(),
-                                                        toastText,
-                                                        LENGTH_LONG)
+                                                        "Clicked sixth item",
+                                                        LENGTH_SHORT)
                                                         .show())
-                                .build());
-            }
+                                .build();
+            default:
+                String titleText = (index + 1) + "th item";
+                String toastText = "Clicked " + (index + 1) + "th item";
+
+                return new GridItem.Builder()
+                        .setImage(new CarIcon.Builder(mIcon).build(),
+                                GridItem.IMAGE_TYPE_ICON)
+                        .setTitle(titleText)
+                        .setOnClickListener(
+                                () ->
+                                        CarToast.makeText(
+                                                getCarContext(),
+                                                toastText,
+                                                LENGTH_SHORT)
+                                                .show())
+                        .build();
+        }
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        int itemLimit = 6;
+        // Adjust the item limit according to the car constrains.
+        if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
+            itemLimit =
+                    Math.min(MAX_GRID_ITEMS,
+                            getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+                                    ConstraintManager.CONTENT_LIMIT_TYPE_GRID));
+        }
+
+        ItemList.Builder gridItemListBuilder = new ItemList.Builder();
+        for (int i = 0; i <= itemLimit; i++) {
+            gridItemListBuilder.addItem(createGridItem(i));
         }
 
         return new GridTemplate.Builder()
@@ -222,7 +225,7 @@
                                                                 CarToast.makeText(
                                                                         getCarContext(),
                                                                         "Clicked Settings",
-                                                                        LENGTH_LONG)
+                                                                        LENGTH_SHORT)
                                                                         .show())
                                                 .build())
                                 .build())
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java
index e5d831b..8a3358c 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PaneTemplateDemoScreen.java
@@ -28,6 +28,7 @@
 import androidx.car.app.CarContext;
 import androidx.car.app.CarToast;
 import androidx.car.app.Screen;
+import androidx.car.app.constraints.ConstraintManager;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarColor;
@@ -71,27 +72,43 @@
         mCommuteIcon = IconCompat.createWithResource(getCarContext(), R.drawable.ic_commute_24px);
     }
 
-    @NonNull
-    @Override
-    public Template onGetTemplate() {
-        Pane.Builder paneBuilder = new Pane.Builder();
-
-        // Add a row with a large image.
-        paneBuilder.addRow(
-                new Row.Builder()
-                        .setTitle("Row with a large image")
+    private Row createRow(int index) {
+        switch (index) {
+            case 0:
+                // Row with a large image.
+                return new Row.Builder()
+                        .setTitle("Row with a large image and long text long text long text long "
+                                + "text long text")
                         .addText("Text text text")
                         .addText("Text text text")
                         .setImage(new CarIcon.Builder(mRowLargeIcon).build())
-                        .build());
-
-        // Add a non-clickable rows.
-        paneBuilder.addRow(
-                new Row.Builder()
-                        .setTitle("Row title")
+                        .build();
+            default:
+                return new Row.Builder()
+                        .setTitle("Row title " + (index + 1))
                         .addText("Row text 1")
                         .addText("Row text 2")
-                        .build());
+                        .build();
+
+        }
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        int listLimit = 4;
+
+        // Adjust the item limit according to the car constrains.
+        if (getCarContext().getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
+            listLimit =
+                    getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+                            ConstraintManager.CONTENT_LIMIT_TYPE_PANE);
+        }
+
+        Pane.Builder paneBuilder = new Pane.Builder();
+        for (int i = 0; i < listLimit; i++) {
+            paneBuilder.addRow(createRow(i));
+        }
 
         // Also set a large image outside of the rows.
         paneBuilder.setImage(new CarIcon.Builder(mPaneImage).build());
diff --git a/compose/compiler/compiler-daemon/OWNERS b/compose/compiler/compiler-daemon/OWNERS
new file mode 100644
index 0000000..4d2a2f1
--- /dev/null
+++ b/compose/compiler/compiler-daemon/OWNERS
@@ -0,0 +1,3 @@
+chuckj@google.com
+diegoperez@google.com
+amaurym@google.com
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/build.gradle b/compose/compiler/compiler-daemon/build.gradle
new file mode 100644
index 0000000..c0e3ebf
--- /dev/null
+++ b/compose/compiler/compiler-daemon/build.gradle
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("kotlin")
+    id("application")
+    id("com.github.johnrengelman.shadow")
+}
+
+mainClassName = "androidx.compose.compiler.daemon.MainKt"
+
+dependencies {
+    implementation(libs.kotlinCompiler)
+    implementation(libs.kotlinStdlib)
+    implementation(project(":compose:compiler:compiler-hosted"))
+}
+
+shadowJar {
+    // The jar file MUST begin with "kotlin-compiler".
+    // If this name is not present, the Kotlin compiler will try to find kotlinHome and might cause
+    // a version mismatch if the dist version does not match the one shipped here.
+    // When the artifact starts with "kotlin-compiler", the compiler will not do that search.
+    archiveBaseName = 'kotlin-compiler-daemon'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs = [
+                // Disable the warning "Some JAR files in the classpath have the [...]".
+                // We include the runtime intentionally as part of the jar.
+                "-Xskip-runtime-version-check",
+        ]
+    }
+}
+
+androidx {
+    name = "Compose Compiler Daemon"
+    type = LibraryType.COMPILER_DAEMON
+    mavenGroup = LibraryGroups.Compose.COMPILER
+    inceptionYear = "2021"
+    description = "Compiler Daemon that includes the Compose plugin"
+}
diff --git a/compose/compiler/compiler-daemon/integration-tests/build.gradle b/compose/compiler/compiler-daemon/integration-tests/build.gradle
new file mode 100644
index 0000000..09ac009
--- /dev/null
+++ b/compose/compiler/compiler-daemon/integration-tests/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("kotlin")
+}
+
+dependencies {
+    testImplementation(libs.junit)
+
+    testImplementation(libs.kotlinCompiler)
+    testImplementation(libs.kotlinStdlib)
+    testImplementation(project(":compose:compiler:compiler-daemon"))
+}
+
+androidx {
+    name = "AndroidX Compiler Daemon CLI Tests"
+    publish = Publish.NONE
+    inceptionYear = "2021"
+    description = "Contains test for the compose compiler daemon"
+}
diff --git a/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/CompilerTest.kt b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/CompilerTest.kt
new file mode 100644
index 0000000..15fa946
--- /dev/null
+++ b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/CompilerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.compiler.daemon
+
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.io.File
+import java.nio.file.Files
+
+@RunWith(Parameterized::class)
+class CompilerTest(
+    @Suppress("UNUSED_PARAMETER") name: String,
+    private val daemonCompiler: DaemonCompiler
+) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data(): Array<Array<Any>> = arrayOf(
+            arrayOf(BasicDaemonCompiler::class.java.name, BasicDaemonCompiler),
+            arrayOf(IncrementalDaemonCompiler::class.java.name, IncrementalDaemonCompiler)
+        )
+    }
+
+    @Test
+    fun `files are compiled successfully`() {
+        val sourceDir = Files.createTempDirectory("source")
+        val inputFile = File(sourceDir.toFile(), "Input.kt").apply {
+            writeText(
+                """
+            fun main() {
+            }
+        """.trimIndent()
+            )
+        }
+        val inputFile2 = File(sourceDir.toFile(), "Input2.kt").apply {
+            writeText(
+                """
+            fun main2() {
+            }
+        """.trimIndent()
+            )
+        }
+
+        run {
+            val outputDir = Files.createTempDirectory("output")
+            assertEquals(
+                ExitCode.OK, daemonCompiler.compile(
+                    arrayOf(
+                        "-d", outputDir.toAbsolutePath().toString(),
+                        inputFile.absolutePath
+                    ), DaemonCompilerSettings(null)
+                )
+            )
+            assertTrue(File(outputDir.toFile(), "InputKt.class").exists())
+        }
+
+        // Verify multiple input files
+        run {
+            val outputDir = Files.createTempDirectory("output")
+            assertEquals(
+                ExitCode.OK, daemonCompiler.compile(
+                    arrayOf(
+                        "-d", outputDir.toAbsolutePath().toString(),
+                        inputFile.absolutePath, inputFile2.absolutePath
+                    ), DaemonCompilerSettings(null)
+                )
+            )
+            assertTrue(File(outputDir.toFile(), "InputKt.class").exists())
+            assertTrue(File(outputDir.toFile(), "Input2Kt.class").exists())
+        }
+    }
+
+    @Test
+    fun `compilation fails with syntax errors`() {
+        val sourceDir = Files.createTempDirectory("source").toFile()
+        val outputDir = Files.createTempDirectory("output")
+        val inputFile = File(sourceDir, "Input.kt")
+        inputFile.writeText(
+            """
+            Invalid code
+        """.trimIndent()
+        )
+
+        assertEquals(
+            ExitCode.COMPILATION_ERROR, daemonCompiler.compile(
+                arrayOf(
+                    "-d", outputDir.toAbsolutePath().toString(),
+                    inputFile.absolutePath
+                ), DaemonCompilerSettings(null)
+            )
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/DaemonProtocolTest.kt b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/DaemonProtocolTest.kt
new file mode 100644
index 0000000..a97752d
--- /dev/null
+++ b/compose/compiler/compiler-daemon/integration-tests/src/test/kotlin/androidx/compose/compiler/daemon/DaemonProtocolTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.compiler.daemon
+
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.ByteArrayOutputStream
+import java.io.OutputStreamWriter
+import java.io.PipedReader
+import java.io.PipedWriter
+import java.io.PrintWriter
+import java.util.concurrent.CountDownLatch
+import kotlin.concurrent.thread
+
+class DaemonProtocolTest {
+
+    @Test
+    fun `verify daemon compiler commands`() {
+        val compileCalls = mutableListOf<String>()
+        val outputWriter = PipedWriter()
+        val daemonOutput = ByteArrayOutputStream()
+        val countDownLatch = CountDownLatch(2)
+        val pipedReader = PipedReader(outputWriter)
+
+        val output = PrintWriter(outputWriter)
+
+        val testDaemon = object : DaemonCompiler {
+            override fun compile(
+                args: Array<String>,
+                daemonCompilerSettings: DaemonCompilerSettings
+            ): ExitCode {
+                val pluginParameter = daemonCompilerSettings.composePluginPath
+                    ?.let { "-plugin $it " } ?: ""
+                val otherArgs = args.joinToString(" ")
+                try {
+                    compileCalls.add("kotlinc $pluginParameter$otherArgs")
+                    return ExitCode.OK
+                } finally {
+                    countDownLatch.countDown()
+                }
+            }
+        }
+
+        thread {
+            startInputLoop(
+                testDaemon,
+                DaemonCompilerSettings("pluginPath1"),
+                pipedReader, OutputStreamWriter(daemonOutput)
+            )
+        }
+        output.println(
+            """
+            -a 1
+            -b
+            Test.kt
+            done
+        """.trimIndent()
+        )
+        output.flush()
+
+        countDownLatch.await()
+        assertEquals(
+            """
+            kotlinc -version
+            kotlinc -plugin pluginPath1 -a 1 -b Test.kt
+        """.trimIndent(), compileCalls.joinToString("\n")
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
new file mode 100644
index 0000000..ebb301a
--- /dev/null
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.compiler.daemon
+
+import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
+import org.jetbrains.kotlin.build.report.BuildReporter
+import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
+import org.jetbrains.kotlin.cli.common.ExitCode
+import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
+import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
+import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
+import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
+import org.jetbrains.kotlin.config.Services
+import org.jetbrains.kotlin.incremental.ClasspathChanges
+import org.jetbrains.kotlin.incremental.EmptyICReporter
+import org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner
+import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory
+import org.jetbrains.kotlin.incremental.withIC
+import java.io.File
+import java.nio.file.Files
+
+internal fun parseArgs(
+    args: Array<String>,
+    composePluginPath: String? = null,
+    compiler: K2JVMCompiler = K2JVMCompiler()
+): K2JVMCompilerArguments {
+    val compilerArgs = compiler.createArguments()
+    compiler.parseArguments(args, compilerArgs)
+
+    composePluginPath?.let {
+        compilerArgs.pluginClasspaths = (compilerArgs.pluginClasspaths ?: emptyArray()) + it
+        compilerArgs.pluginOptions =
+            (compilerArgs.pluginOptions ?: emptyArray()) +
+                "plugin:androidx.compose.plugins.idea:enabled=true"
+        compilerArgs.useIR = true // Force IR since it's required for Compose
+    }
+    return compilerArgs
+}
+
+data class DaemonCompilerSettings(val composePluginPath: String? = null) {
+    companion object {
+        val DefaultSettings = DaemonCompilerSettings()
+    }
+}
+
+interface DaemonCompiler {
+    fun compile(
+        args: Array<String>,
+        daemonCompilerSettings: DaemonCompilerSettings = DaemonCompilerSettings.DefaultSettings
+    ): ExitCode
+}
+
+/**
+ * A [DaemonCompiler] that uses regular `kotlinc` invocations.
+ */
+object BasicDaemonCompiler : DaemonCompiler {
+    private val compiler = K2JVMCompiler()
+
+    override fun compile(
+        args: Array<String>,
+        daemonCompilerSettings: DaemonCompilerSettings
+    ): ExitCode {
+        return try {
+            val compilerArgs = parseArgs(args, daemonCompilerSettings.composePluginPath)
+            compiler.exec(
+                PrintingMessageCollector(System.err, MessageRenderer.PLAIN_RELATIVE_PATHS, true),
+                Services.EMPTY,
+                compilerArgs
+            )
+        } catch (t: Throwable) {
+            t.printStackTrace(System.err)
+            ExitCode.INTERNAL_ERROR
+        }
+    }
+}
+
+/**
+ * A [DaemonCompiler] that calls `kotlinc` in incremental mode.
+ */
+object IncrementalDaemonCompiler : DaemonCompiler {
+    private val compiler = IncrementalJvmCompilerRunner(
+        workingDir = Files.createTempDirectory("workingDir").toFile(),
+        reporter = BuildReporter(EmptyICReporter, DoNothingBuildMetricsReporter),
+        usePreciseJavaTracking = true,
+        outputFiles = emptyList(),
+        buildHistoryFile = Files.createTempFile("build-history", ".bin").toFile(),
+        modulesApiHistory = EmptyModulesApiHistory,
+        kotlinSourceFilesExtensions = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
+        classpathChanges = ClasspathChanges.NotAvailable.UnableToCompute
+    )
+
+    override fun compile(
+        args: Array<String>,
+        daemonCompilerSettings: DaemonCompilerSettings
+    ): ExitCode {
+        println("Incremental compiler invoke")
+        return try {
+            val compilerArgs = parseArgs(args, daemonCompilerSettings.composePluginPath)
+            compilerArgs.moduleName = "test"
+            withIC {
+                compiler.compile(
+                    compilerArgs.freeArgs.map { File(it) },
+                    compilerArgs,
+                    PrintingMessageCollector(
+                        System.err,
+                        MessageRenderer.PLAIN_RELATIVE_PATHS,
+                        true
+                    ),
+                    null,
+                    null
+                )
+            }
+        } catch (t: Throwable) {
+            t.printStackTrace(System.err)
+            ExitCode.INTERNAL_ERROR
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/DaemonProtocol.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/DaemonProtocol.kt
new file mode 100644
index 0000000..f4d6970
--- /dev/null
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/DaemonProtocol.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.compiler.daemon
+
+import java.io.BufferedReader
+import java.io.PrintWriter
+import java.io.Reader
+import java.io.Writer
+import kotlin.system.exitProcess
+
+/**
+ * Starts the input loop listening for commands to be sent to the [daemonCompiler] using the given
+ * [daemonSettings]. This method will listen to input commands via [inputStream] and sending the
+ * output to [outputStream].
+ */
+fun startInputLoop(
+    daemonCompiler: DaemonCompiler,
+    daemonSettings: DaemonCompilerSettings,
+    inputReader: Reader,
+    outputWriter: Writer
+) {
+    val input = BufferedReader(inputReader)
+    val output = PrintWriter(outputWriter)
+    // Show the version and trigger the loading of all the compiler classes
+    daemonCompiler.compile(arrayOf("-version"))
+    while (true) {
+        val commandLineBuilder = mutableListOf<String>()
+        while (commandLineBuilder.lastOrNull() != "done") {
+            output.print(">")
+            val line = input.readLine()!!
+            output.println(line)
+            if (line == "quit") exitProcess(1)
+            commandLineBuilder.add(line)
+        }
+        val exitCode = daemonCompiler.compile(
+            commandLineBuilder.dropLast(1).toTypedArray(), daemonSettings
+        )
+        output.println("RESULT ${exitCode.code}")
+        output.flush()
+    }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/main.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/main.kt
new file mode 100644
index 0000000..6a504b2
--- /dev/null
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/main.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.compiler.daemon
+
+import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback
+import java.io.PrintWriter
+import java.nio.charset.Charset
+
+private data class CliOptions(
+    val disableEmbeddedPlugin: Boolean = false,
+    val useIncrementalCompiler: Boolean = false,
+    val studioVersion: String? = null
+)
+
+private fun parseArgumentWithOption(args: Array<String>, argumentName: String): String? {
+    val argumentIndex = args.indexOf(argumentName)
+    return if (argumentIndex != -1) args[argumentIndex + 1] else null
+}
+
+private fun parseCliOptions(args: Array<String>): CliOptions {
+    return CliOptions(
+        // Disables the use of the plugin embedded with the jar
+        disableEmbeddedPlugin = args.contains("-disableEmbedded"),
+        useIncrementalCompiler = args.contains("-incremental"),
+        studioVersion = parseArgumentWithOption(args, "-studio")
+    )
+}
+
+fun main(args: Array<String>) {
+    setIdeaIoUseFallback()
+
+    val cliOptions = parseCliOptions(args)
+    println(cliOptions)
+
+    val jarPath = if (cliOptions.disableEmbeddedPlugin)
+        null
+    else
+        object {}.javaClass.protectionDomain.codeSource.location.path
+    println(jarPath?.let { "Using embedded plugin with path $it" } ?: "No embedded plugin")
+
+    val compiler: DaemonCompiler = if (cliOptions.useIncrementalCompiler) {
+        println("Using IncrementalDaemonCompiler")
+        IncrementalDaemonCompiler
+    } else {
+        println("Using BasicDaemonCompiler")
+        BasicDaemonCompiler
+    }
+    // Show the version and trigger the loading of all the compiler classes
+    compiler.compile(arrayOf("-version"))
+    val settings = DaemonCompilerSettings(jarPath)
+    startInputLoop(compiler,
+        settings,
+        System.`in`.bufferedReader(Charset.defaultCharset()),
+        PrintWriter(System.out)
+    )
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index 2373eb8..cdaa18f 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -549,7 +549,7 @@
               if (%default and 0b0001 !== 0) {
                 %dirty = %dirty or 0b0010
               }
-              if (%default.inv() and 0b0001 !== 0 || %dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%default and 0b0001 !== 0b0001 || %dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   y = null
                 }
@@ -755,7 +755,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 A(Wrapper(value), %composer, Wrapper.%stable or 0b1000 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -794,7 +794,7 @@
                 if (%changed and 0b1110 === 0) {
                   %dirty = %dirty or if (%composer.changed(item)) 0b0100 else 0b0010
                 }
-                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                   A(item, %composer, 0b1110 and %dirty)
                   A(Wrapper(item), %composer, Wrapper.%stable or 0)
                 } else {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 0417a35..e4b759f 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -330,7 +330,7 @@
               Example({ %composer: Composer?, %changed: Int ->
                 %composer.startReplaceableGroup(<>)
                 sourceInformation(%composer, "C:Test.kt#2487m")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -429,7 +429,7 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(block)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                     block(%composer, 0b1110 and %dirty)
                   } else {
                     %composer.skipToGroupEnd()
@@ -446,7 +446,7 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                     used(text)
                   } else {
                     %composer.skipToGroupEnd()
@@ -463,12 +463,12 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                     %composer.startMovableGroup(<>, value)
                     sourceInformation(%composer, "<Wrappe...>")
                     Wrapper(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
                       sourceInformation(%composer, "C<Leaf("...>:Test.kt#2487m")
-                      if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                      if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                         Leaf("Value %value", %composer, 0)
                       } else {
                         %composer.skipToGroupEnd()
@@ -551,15 +551,15 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(composable)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                     emit({ %composer: Composer?, %changed: Int ->
                       %composer.startReplaceableGroup(<>)
                       sourceInformation(%composer, "C<emit>:Test.kt#2487m")
-                      if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                      if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                         emit({ %composer: Composer?, %changed: Int ->
                           %composer.startReplaceableGroup(<>)
                           sourceInformation(%composer, "C<compos...>:Test.kt#2487m")
-                          if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                          if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                             composable(%composer, 0b1110 and %dirty)
                           } else {
                             %composer.skipToGroupEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 84c8513..aefd2e6 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -2260,7 +2260,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 val tmp0_safe_receiver = x
                 %composer.startReplaceableGroup(<>)
                 sourceInformation(%composer, "*<A(b)>")
@@ -2315,7 +2315,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 x?.let { it: Int ->
                   if (it > 0) {
                     NA()
@@ -2424,7 +2424,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C<A()>:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   A(%composer, 0)
                 } else {
                   %composer.skipToGroupEnd()
@@ -2453,7 +2453,7 @@
                 IW({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<A()>:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                   } else {
                     %composer.skipToGroupEnd()
@@ -2502,7 +2502,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C<effect>:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "*<effect>")
                   repeat(number) { it: Int ->
@@ -2551,7 +2551,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(<unsafe-coerce>(value))) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 used(value)
                 A(%composer, 0)
               } else {
@@ -2730,7 +2730,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2759,7 +2759,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2788,7 +2788,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2817,7 +2817,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2846,7 +2846,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2875,7 +2875,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2904,7 +2904,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2933,7 +2933,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2962,7 +2962,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -2991,7 +2991,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3020,7 +3020,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3049,7 +3049,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3078,7 +3078,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3107,7 +3107,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3136,7 +3136,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3165,7 +3165,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3194,7 +3194,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3223,7 +3223,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3252,7 +3252,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3281,7 +3281,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3310,7 +3310,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3339,7 +3339,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3368,7 +3368,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3397,7 +3397,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 used(p0)
                 used(p1)
                 used(p2)
@@ -3439,7 +3439,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(<unsafe-coerce>(value))) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 used(value)
               } else {
                 %composer.skipToGroupEnd()
@@ -3525,7 +3525,7 @@
               fun onCreate() {
                 setContent(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
                   sourceInformation(%composer, "C<B(a)>,<B(a)>:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     B(a, %composer, 0)
                     B(a, %composer, 0)
                   } else {
@@ -3540,7 +3540,7 @@
               var a = "Test"
               setContent(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C<B(a)>,<B(a)>:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   B(a, %composer, 0)
                   B(a, %composer, 0)
                 } else {
@@ -3594,11 +3594,11 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C<IW>:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   IW({ %composer: Composer?, %changed: Int ->
                     %composer.startReplaceableGroup(<>)
                     sourceInformation(%composer, "C<T(2)>,<T(4)>:Test.kt")
-                    if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                    if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                       T(2, %composer, 0b0110)
                       %composer.startReplaceableGroup(<>)
                       sourceInformation(%composer, "*<T(3)>")
@@ -3673,7 +3673,7 @@
                 Layout({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<Text("...>:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     Text("%c %cl", %composer, 0)
                   } else {
                     %composer.skipToGroupEnd()
@@ -3782,7 +3782,7 @@
                 Layout({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<Text("...>:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     Text("%c %cl", %composer, 0)
                   } else {
                     %composer.skipToGroupEnd()
@@ -3863,7 +3863,7 @@
                 Wrapper({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C*<Leaf(0...>:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     repeat(1) { it: Int ->
                       %composer.startReplaceableGroup(<>)
                       sourceInformation(%composer, "*<Leaf(0...>")
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
index e907da4..750f428 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
@@ -100,7 +100,7 @@
             }
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   A(%composer, 0)
                 } else {
                   %composer.skipToGroupEnd()
@@ -127,7 +127,7 @@
               sourceInformation(%composer, "C(Test)")
               if (%changed !== 0 || !%composer.skipping) {
                 IW({ %composer: Composer?, %changed: Int ->
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                   } else {
                     %composer.skipToGroupEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index 65c1383..becc6b8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -105,7 +105,7 @@
               } else if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(<unsafe-coerce>(foo))) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   foo = Foo(0)
                 }
@@ -175,7 +175,7 @@
             fun Bar(unused: Function2<Composer, Int, Unit> = { %composer: Composer?, %changed: Int ->
               %composer.startReplaceableGroup(<>)
               sourceInformation(%composer, "C:Test.kt")
-              if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                 Unit
               } else {
                 %composer.skipToGroupEnd()
@@ -210,7 +210,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -260,7 +260,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(b)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -525,7 +525,7 @@
               } else if (%changed3 and 0b1110 === 0) {
                 %dirty3 = %dirty3 or if (%composer.changed(a30)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty2 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty3 and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty2 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty3 and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   a00 = 0
                 }
@@ -903,7 +903,7 @@
               } else if (%changed3 and 0b01110000 === 0) {
                 %dirty3 = %dirty3 or if (%composer.changed(a31)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty2 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty3 and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty2 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty3 and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   a00 = 0
                 }
@@ -1282,7 +1282,7 @@
               if (%changed3 and 0b01110000 === 0) {
                 %dirty3 = %dirty3 or if (%default1 and 0b0001 === 0 && %composer.changed(a31)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty2 and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty3 and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty2 and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty3 and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index 970a2ed..9953eff 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -85,7 +85,7 @@
               } else if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   x = 0
                 }
@@ -95,7 +95,7 @@
                 used(y)
                 Wrap(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
                   sourceInformation(%composer, "C:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     if (x > 0) {
                       %composer.startReplaceableGroup(<>)
                       sourceInformation(%composer, "<A(x)>")
@@ -162,7 +162,7 @@
               } else if (%changed and 0b001110000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(<unsafe-coerce>(overflow))) 0b000100000000 else 0b10000000
               }
-              if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   style = Companion.Default
                 }
@@ -212,7 +212,7 @@
               } else if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(arrangement)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   arrangement = Arrangement.Top
                 }
@@ -250,7 +250,7 @@
               } else if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   content = ComposableSingletons%TestKt.lambda-1
                 }
@@ -265,7 +265,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -300,7 +300,7 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                     a.compute(it, %composer, 0b1110 and %dirty)
                   } else {
                     %composer.skipToGroupEnd()
@@ -351,7 +351,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(colors)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 Text("hello world", null, colors.getColor(%composer, 0b1110 and %dirty), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, null, null, %composer, 0b0110, 0, 0b1111111111111010)
               } else {
                 %composer.skipToGroupEnd()
@@ -503,7 +503,7 @@
               } else if (%changed and 0b01110000000000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b00100000000000000000 else 0b00010000000000000000
               }
-              if (%dirty and 0b01011011011011011011 xor 0b00010010010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011011011011011 !== 0b00010010010010010010 || !%composer.skipping) {
                 if (%default and 0b0010 !== 0) {
                   modifier = Companion
                 }
@@ -554,7 +554,7 @@
               } else if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   modifier = Companion
                 }
@@ -604,7 +604,7 @@
               } else if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(modifier)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   modifier = Companion
                 }
@@ -639,7 +639,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(a)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -687,7 +687,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 @Composable
                 fun Inner(%composer: Composer?, %changed: Int) {
                   %composer.startReplaceableGroup(<>)
@@ -786,7 +786,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(shape)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -848,7 +848,7 @@
               } else if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   modifier = Companion
                 }
@@ -867,7 +867,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -904,7 +904,7 @@
                 if (%changed and 0b01110000 === 0) {
                   %dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
                 }
-                if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+                if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
                   A(x, %composer, 0b1110 and %dirty)
                   B(y, %composer, 0b1110 and %dirty shr 0b0011)
                 } else {
@@ -963,7 +963,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 content(%composer, 0b1110 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -988,7 +988,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   val id = object
                 } else {
                   %composer.skipToGroupEnd()
@@ -1024,7 +1024,7 @@
               if (%dirty and 0b1110 === 0) {
                 %dirty = %dirty or 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 print(values)
               } else {
                 %composer.skipToGroupEnd()
@@ -1064,7 +1064,7 @@
               if (%dirty and 0b1110 === 0) {
                 %dirty = %dirty or 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 print(values)
               } else {
                 %composer.skipToGroupEnd()
@@ -1180,7 +1180,7 @@
               } else if (%changed and 0b001110000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(c)) 0b000100000000 else 0b10000000
               }
-              if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -1243,7 +1243,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 content(y, %composer, 0b1110 and %dirty or 0b01110000 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -1267,7 +1267,7 @@
               } else if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   x = 0
                 }
@@ -1281,7 +1281,7 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                     used(it)
                     A(x, 0, %composer, 0b1110 and %dirty, 0b0010)
                   } else {
@@ -1348,7 +1348,7 @@
                 if (%changed and 0b1110 === 0) {
                   %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
                 }
-                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                   A(x, 0, %composer, 0b1110 and %dirty, 0b0010)
                 } else {
                   %composer.skipToGroupEnd()
@@ -1400,7 +1400,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(y)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 A(x, y, %composer, 0b1110 and %dirty or 0b01110000 and %dirty, 0)
               } else {
                 %composer.skipToGroupEnd()
@@ -1446,7 +1446,7 @@
               if (%default and 0b0010 !== 0) {
                 %dirty = %dirty or 0b00010000
               }
-              if (%default.inv() and 0b0010 !== 0 || %dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%default and 0b0010 !== 0b0010 || %dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -1517,7 +1517,7 @@
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(CanSkip):Test.kt")
               val %dirty = %changed
-              if (%default.inv() and 0b0001 !== 0 || %dirty and 0b0001 !== 0 || !%composer.skipping) {
+              if (%default and 0b0001 !== 0b0001 || %dirty and 0b0001 !== 0 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -1591,7 +1591,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 A(x, %composer, 0b1110 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -1633,7 +1633,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 B(text, <unsafe-coerce>(0), %composer, 0b1110 and %dirty, 0b0010)
               } else {
                 %composer.skipToGroupEnd()
@@ -1657,7 +1657,7 @@
               } else if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(<unsafe-coerce>(color))) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 if (%default and 0b0010 !== 0) {
                   color = Companion.Unset
                 }
@@ -1774,7 +1774,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -1811,7 +1811,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -1844,7 +1844,7 @@
               } else if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   x = 0
                 }
@@ -1880,7 +1880,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -1951,7 +1951,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -2015,7 +2015,7 @@
               if (%default and 0b00010000 !== 0) {
                 %dirty = %dirty or 0b0010000000000000
               }
-              if (%default.inv() and 0b00010000 !== 0 || %dirty and 0b1011011011011011 xor 0b0010010010010010 !== 0 || !%composer.skipping) {
+              if (%default and 0b00010000 !== 0b00010000 || %dirty and 0b1011011011011011 !== 0b0010010010010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0100 !== 0) {
@@ -2070,7 +2070,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 X(x + 1, %composer, 0)
                 X(x, %composer, 0b1110 and %dirty)
               } else {
@@ -2128,7 +2128,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: @[ExtensionFunctionType] Function5<LazyItemScope, Int, User?, Composer, Int, Unit> = composableLambdaInstance(<>, false) { index: Int, user: User?, %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b0001010000000001 xor 0b010000000000 !== 0 || !%composer.skipping) {
+                if (%changed and 0b0001010000000001 !== 0b010000000000 || !%composer.skipping) {
                   print("Hello World")
                 } else {
                   %composer.skipToGroupEnd()
@@ -2159,7 +2159,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(<this>)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 doSomething(<this>, %composer, 0b1110 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -2261,7 +2261,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 B(x, x + 1, 123, fooGlobal, %composer, 0b110110000000 or 0b1110 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -2302,7 +2302,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: @[ExtensionFunctionType] Function3<Foo, Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+                if (%changed and 0b01010001 !== 0b00010000 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -2314,7 +2314,7 @@
                 if (%changed and 0b1110 === 0) {
                   %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
                 }
-                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                   used(%this%null.x)
                 } else {
                   %composer.skipToGroupEnd()
@@ -2322,7 +2322,7 @@
               }
               val lambda-3: @[ExtensionFunctionType] Function3<StableFoo, Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+                if (%changed and 0b01010001 !== 0b00010000 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -2334,7 +2334,7 @@
                 if (%changed and 0b1110 === 0) {
                   %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
                 }
-                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                   used(%this%null.x)
                 } else {
                   %composer.skipToGroupEnd()
@@ -2371,21 +2371,21 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 Provide(composableLambda(%composer, <>, true) { y: Int, %composer: Composer?, %changed: Int ->
                   sourceInformation(%composer, "C<Provid...>,<B(x,>:Test.kt")
                   val %dirty = %changed
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(y)) 0b0100 else 0b0010
                   }
-                  if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                     Provide(composableLambda(%composer, <>, true) { z: Int, %composer: Composer?, %changed: Int ->
                       sourceInformation(%composer, "C<B(x,>:Test.kt")
                       val %dirty = %changed
                       if (%changed and 0b1110 === 0) {
                         %dirty = %dirty or if (%composer.changed(z)) 0b0100 else 0b0010
                       }
-                      if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                      if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                         B(x, y, z, %composer, 0b1110 and %dirty or 0b01110000 and %dirty shl 0b0011 or 0b001110000000 and %dirty shl 0b0110, 0)
                       } else {
                         %composer.skipToGroupEnd()
@@ -2430,7 +2430,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 @Composable
                 fun foo(y: Int, %composer: Composer?, %changed: Int) {
                   %composer.startReplaceableGroup(<>)
@@ -2592,7 +2592,7 @@
               } else if (%changed1 and 0b1110000000000000 === 0) {
                 %dirty1 = %dirty1 or if (%composer.changed(a14)) 0b0100000000000000 else 0b0010000000000000
               }
-              if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b1011011011011011 xor 0b0010010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b1011011011011011 !== 0b0010010010010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   a00 = 0
                 }
@@ -2801,7 +2801,7 @@
               } else if (%changed1 and 0b01110000000000000000 === 0) {
                 %dirty1 = %dirty1 or if (%composer.changed(a15)) 0b00100000000000000000 else 0b00010000000000000000
               }
-              if (%dirty and 0b01011011011011011011011011011011 xor 0b00010010010010010010010010010010 !== 0 || %dirty1 and 0b01011011011011011011 xor 0b00010010010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011011011011011011011011011 !== 0b00010010010010010010010010010010 || %dirty1 and 0b01011011011011011011 !== 0b00010010010010010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   a00 = 0
                 }
@@ -2947,7 +2947,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%default and 0b0010 === 0 && %composer.changed(mightChange)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0001 !== 0) {
@@ -2994,7 +2994,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 content(%composer, 0b1110 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
@@ -3067,7 +3067,7 @@
               } else if (%changed and 0b001110000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b000100000000 else 0b10000000
               }
-              if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
                 if (%default and 0b0001 !== 0) {
                   modifier = Companion
                 }
@@ -3090,7 +3090,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -3126,7 +3126,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(cond)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.startReplaceableGroup(<>)
                 sourceInformation(%composer, "<A()>")
                 if (cond) {
@@ -3187,7 +3187,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(b)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01010001 !== 0b00010000 || !%composer.skipping) {
                 used(b)
               } else {
                 %composer.skipToGroupEnd()
@@ -3204,7 +3204,7 @@
               if (%changed and 0b001110000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(c)) 0b000100000000 else 0b10000000
               }
-              if (%dirty and 0b001010000001 xor 0b10000000 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b001010000001 !== 0b10000000 || !%composer.skipping) {
                 used(c)
               } else {
                 %composer.skipToGroupEnd()
@@ -3255,7 +3255,7 @@
               if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 used(<this>)
                 used(x)
               } else {
@@ -3276,7 +3276,7 @@
                 if (%changed and 0b01110000 === 0) {
                   %dirty = %dirty or if (%composer.changed(it)) 0b00100000 else 0b00010000
                 }
-                if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+                if (%dirty and 0b001011011011 !== 0b10010010 || !%composer.skipping) {
                   used(%this%null)
                   used(it)
                 } else {
@@ -3320,7 +3320,7 @@
               if (%dirty and 0b01110000 === 0) {
                 %dirty = %dirty or 0b00010000
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.startDefaults()
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   if (%default and 0b0010 !== 0) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index d37af66..77ca5c9 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -38,7 +38,7 @@
               val b: String = ""
               val c: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   print(b)
                 } else {
                   %composer.skipToGroupEnd()
@@ -89,7 +89,7 @@
                   sourceInformation(%composer, "C(C)<B>:Test.kt")
                   B(composableLambda(%composer, <>, false) { %composer: Composer?, %changed: Int ->
                     sourceInformation(%composer, "C<A()>:Test.kt")
-                    if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                    if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                       A(%composer, 0)
                     } else {
                       %composer.skipToGroupEnd()
@@ -297,7 +297,7 @@
                 <<LOCALDELPROP>>
                 B(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
                   sourceInformation(%composer, "C:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     print(<get-x>())
                   } else {
                     %composer.skipToGroupEnd()
@@ -332,7 +332,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -340,7 +340,7 @@
               }
               val lambda-2: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -384,7 +384,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -392,7 +392,7 @@
               }
               val lambda-2: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -432,7 +432,7 @@
             internal object ComposableSingletons%TestKt {
               val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
                 sourceInformation(%composer, "C:Test.kt")
-                if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -475,11 +475,11 @@
               } else if (%changed and 0b01110000 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b00100000 else 0b00010000
               }
-              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
                 if (%default and 0b0010 !== 0) {
                   content = composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
                     sourceInformation(%composer, "C<Displa...>:Test.kt")
-                    if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                    if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                       Display("%enabled", %composer, 0)
                     } else {
                       %composer.skipToGroupEnd()
@@ -525,10 +525,10 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(enabled)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 val content = composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
                   sourceInformation(%composer, "C<Displa...>:Test.kt")
-                  if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     Display("%enabled", %composer, 0)
                   } else {
                     %composer.skipToGroupEnd()
@@ -578,7 +578,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 content()
               } else {
                 %composer.skipToGroupEnd()
@@ -632,7 +632,7 @@
           if (%changed and 0b1110 === 0) {
             %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
           }
-          if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+          if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
             content()
           } else {
             %composer.skipToGroupEnd()
@@ -649,7 +649,7 @@
           if (%changed and 0b1110 === 0) {
             %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
           }
-          if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+          if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
             TestLambda(remember(a, {
               {
                 println("Captures a" + a)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 89aa6c0..b6b2d95 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -153,7 +153,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.cache(%dirty and 0b1110 === 0b0100) {
                   val tmp0_return = 1
                   tmp0_return
@@ -185,7 +185,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 %composer.cache(%dirty and 0b1110 === 0b0100 || %dirty and 0b1000 !== 0 && %composer.changed(x)) {
                   val tmp0_return = 1
                   tmp0_return
@@ -529,7 +529,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 A(%composer, 0)
                 if (condition) {
                   val foo = %composer.cache(false) {
@@ -571,7 +571,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (condition) {
                   A(%composer, 0)
                   val foo = remember({
@@ -725,7 +725,7 @@
               if (%changed and 0b0001110000000000 === 0) {
                 %dirty = %dirty or if (%composer.changed(d)) 0b100000000000 else 0b010000000000
               }
-              if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b0001011011011011 !== 0b010010010010 || !%composer.skipping) {
                 val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or %dirty and 0b01110000 === 0b00100000 or %dirty and 0b001110000000 === 0b000100000000 or %dirty and 0b0001110000000000 === 0b100000000000) {
                   val tmp0_return = Foo()
                   tmp0_return
@@ -790,7 +790,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(<unsafe-coerce>(inlineInt))) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 val a = InlineInt(123)
                 val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or false) {
                   val tmp0_return = Foo()
@@ -873,7 +873,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 val b = someInt()
                 val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or %composer.changed(b)) {
                   val tmp0_return = Foo(a, b)
@@ -941,7 +941,7 @@
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%default and 0b0001 === 0 && %composer.changed(a)) 0b0100 else 0b0010
               }
-              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
                 if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
                   %composer.startDefaults()
                   if (%default and 0b0001 !== 0) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
index a056dcd..7ec98da 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
@@ -67,7 +67,7 @@
           if (%changed and 0b1110 === 0) {
             %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
           }
-          if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+          if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
             A(x, %composer, 0b1110 and %dirty)
             A(Foo(0), %composer, 0)
             A(remember({
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index fd485fd..64584940 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -204,7 +204,7 @@
             project: Project,
             configuration: CompilerConfiguration
         ) {
-            val KOTLIN_VERSION_EXPECTATION = "1.6.0"
+            val KOTLIN_VERSION_EXPECTATION = "1.6.10"
             KotlinCompilerVersion.getVersion()?.let { version ->
                 val suppressKotlinVersionCheck = configuration.get(
                     ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index c23d8691..909607f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -364,7 +364,7 @@
  *       if ($changed and 0b0110 === 0) {
  *         $dirty = $dirty or if ($composer.changed(x)) 0b0010 else 0b0100
  *       }
- *      if (%dirty and 0b1011 xor 0b1010 !== 0 || !$composer.skipping) {
+ *      if (%dirty and 0b1011 !== 0b1010 || !$composer.skipping) {
  *        f(x)
  *      } else {
  *        $composer.skipToGroupEnd()
@@ -3055,7 +3055,7 @@
         // (the shift amount represented here by `x`, `y`, and `z`).
 
         // TODO: we could make some small optimization here if we have multiple values passed
-        //  from one function into another in the same order. This may not happen commonly eugh
+        //  from one function into another in the same order. This may not happen commonly enough
         //  to be worth the complication though.
 
         // NOTE: we start with 0b0 because it is important that the low bit is always 0
@@ -3392,12 +3392,13 @@
             }
         }
 
-        return if (resultsWithCalls == 1) {
-            transformed.asCoalescableGroup(resultScopes.single { it.hasComposableCalls })
-        } else if (needsWrappingGroup) {
-            transformed.asCoalescableGroup(whenScope)
-        } else {
-            transformed
+        return when {
+            resultsWithCalls == 1 ->
+                transformed.asCoalescableGroup(resultScopes.single { it.hasComposableCalls })
+            needsWrappingGroup ->
+                transformed.asCoalescableGroup(whenScope)
+            else ->
+                transformed
         }
     }
 
@@ -3916,13 +3917,13 @@
                 val end = min(start + BITS_PER_INT, count)
                 val unstableMask = bitMask(*unstable.sliceArray(start until end))
                 irNotEqual(
-                    // ~$default and unstableMask will be non-zero if any parameters were
-                    // *provided* AND *unstable*
+                    // $default and unstableMask will be different from unstableMask
+                    // iff any parameters were *provided* AND *unstable*
                     irAnd(
-                        irInv(irGet(param)),
+                        irGet(param),
                         irConst(unstableMask)
                     ),
-                    irConst(0)
+                    irConst(unstableMask)
                 )
             }
             return if (expressions.size == 1)
@@ -4042,16 +4043,13 @@
                         irConst(0)
                     )
                 } else {
-                    // $dirty and (0b 101 ... 101 1) xor (0b 001 ... 001 0)
+                    // $dirty and (0b 101 ... 101 1) != (0b 001 ... 001 0)
                     irNotEqual(
-                        irXor(
-                            irAnd(
-                                irGet(param),
-                                irConst(lhs or 0b1)
-                            ),
-                            irConst(rhs or 0b0)
+                        irAnd(
+                            irGet(param),
+                            irConst(lhs or 0b1)
                         ),
-                        irConst(0) // anything non-zero means we have differences
+                        irConst(rhs or 0b0)
                     )
                 }
             }
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 61bdcde..efedd5d 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -373,8 +373,10 @@
 package androidx.compose.foundation.lazy {
 
   public final class LazyDslKt {
-    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
     method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 5a7a948..707477a 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -428,8 +428,10 @@
   }
 
   public final class LazyDslKt {
-    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
     method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -446,7 +448,7 @@
   }
 
   public final class LazyGridKt {
-    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
     method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? spans, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? spans, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.compose.foundation.lazy.GridItemSpan>? spans, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 61bdcde..efedd5d 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -373,8 +373,10 @@
 package androidx.compose.foundation.lazy {
 
   public final class LazyDslKt {
-    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,? extends kotlin.Unit> content);
     method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.compose.foundation.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
new file mode 100644
index 0000000..838e3f6
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.foundation.demos
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Card
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun LazyColumnDragAndDropDemo() {
+    var list by remember { mutableStateOf(List(50) { it }) }
+
+    val listState = rememberLazyListState()
+    val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
+        list = list.toMutableList().apply {
+            add(toIndex, removeAt(fromIndex))
+        }
+    }
+
+    LazyColumn(
+        modifier = Modifier.dragContainer(dragDropState),
+        state = listState,
+        contentPadding = PaddingValues(16.dp),
+        verticalArrangement = Arrangement.spacedBy(16.dp)
+    ) {
+        itemsIndexed(list, key = { _, item -> item }) { index, item ->
+            DraggableItem(dragDropState, index) { isDragging ->
+                val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
+                Card(elevation = elevation) {
+                    Text("Item $item", Modifier.fillMaxWidth().padding(20.dp))
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun rememberDragDropState(
+    lazyListState: LazyListState,
+    onMove: (Int, Int) -> Unit
+): DragDropState {
+    val scope = rememberCoroutineScope()
+    val state = remember(lazyListState) {
+        DragDropState(
+            state = lazyListState,
+            onMove = onMove,
+            scope = scope
+        )
+    }
+    LaunchedEffect(state) {
+        while (true) {
+            val diff = state.scrollChannel.receive()
+            lazyListState.scrollBy(diff)
+        }
+    }
+    return state
+}
+
+class DragDropState internal constructor(
+    private val state: LazyListState,
+    private val scope: CoroutineScope,
+    private val onMove: (Int, Int) -> Unit
+) {
+    var draggingItemIndex by mutableStateOf<Int?>(null)
+        private set
+
+    internal val scrollChannel = Channel<Float>()
+
+    private var draggingItemDraggedDelta by mutableStateOf(0f)
+    private var draggingItemInitialOffset by mutableStateOf(0)
+    internal val draggingItemOffset: Float
+        get() = draggingItemLayoutInfo?.let { item ->
+            draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
+        } ?: 0f
+
+    private val draggingItemLayoutInfo: LazyListItemInfo?
+        get() = state.layoutInfo.visibleItemsInfo
+            .firstOrNull { it.index == draggingItemIndex }
+
+    internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
+        private set
+    internal var previousItemOffset = Animatable(0f)
+        private set
+
+    internal fun onDragStart(offset: Offset) {
+        state.layoutInfo.visibleItemsInfo
+            .firstOrNull { item ->
+                offset.y.toInt() in item.offset..(item.offset + item.size)
+            }?.also {
+                draggingItemIndex = it.index
+                draggingItemInitialOffset = it.offset
+            }
+    }
+
+    internal fun onDragInterrupted() {
+        if (draggingItemIndex != null) {
+            previousIndexOfDraggedItem = draggingItemIndex
+            val startOffset = draggingItemOffset
+            scope.launch {
+                previousItemOffset.snapTo(startOffset)
+                previousItemOffset.animateTo(
+                    0f,
+                    spring(
+                        stiffness = Spring.StiffnessMediumLow,
+                        visibilityThreshold = 1f
+                    )
+                )
+                previousIndexOfDraggedItem = null
+            }
+        }
+        draggingItemDraggedDelta = 0f
+        draggingItemIndex = null
+        draggingItemInitialOffset = 0
+    }
+
+    internal fun onDrag(offset: Offset) {
+        draggingItemDraggedDelta += offset.y
+
+        val draggingItem = draggingItemLayoutInfo ?: return
+        val startOffset = draggingItem.offset + draggingItemOffset
+        val endOffset = startOffset + draggingItem.size
+
+        val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
+            if (item.offsetEnd > startOffset && item.offset < endOffset &&
+                draggingItem.index != item.index
+            ) {
+                val delta = startOffset - draggingItem.offset
+                when {
+                    delta > 0 -> (endOffset > item.offsetEnd)
+                    else -> (startOffset < item.offset)
+                }
+            } else {
+                false
+            }
+        }
+        if (targetItem != null) {
+            val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
+                draggingItem.index
+            } else if (draggingItem.index == state.firstVisibleItemIndex) {
+                targetItem.index
+            } else {
+                null
+            }
+            if (scrollToIndex != null) {
+                scope.launch {
+                    // this is needed to neutralize automatic keeping the first item first.
+                    state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+                    onMove.invoke(draggingItem.index, targetItem.index)
+                }
+            } else {
+                onMove.invoke(draggingItem.index, targetItem.index)
+            }
+            draggingItemIndex = targetItem.index
+        } else {
+            val overscroll = when {
+                draggingItemDraggedDelta > 0 ->
+                    (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
+                draggingItemDraggedDelta < 0 ->
+                    (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
+                else -> 0f
+            }
+            if (overscroll != 0f) {
+                scrollChannel.trySend(overscroll)
+            }
+        }
+    }
+
+    private val LazyListItemInfo.offsetEnd: Int
+        get() = this.offset + this.size
+}
+
+fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
+    return pointerInput(dragDropState) {
+        detectDragGesturesAfterLongPress(
+            onDrag = { change, offset ->
+                change.consumeAllChanges()
+                dragDropState.onDrag(offset = offset)
+            },
+            onDragStart = { offset -> dragDropState.onDragStart(offset) },
+            onDragEnd = { dragDropState.onDragInterrupted() },
+            onDragCancel = { dragDropState.onDragInterrupted() }
+        )
+    }
+}
+
+@ExperimentalFoundationApi
+@Composable
+fun LazyItemScope.DraggableItem(
+    dragDropState: DragDropState,
+    index: Int,
+    modifier: Modifier = Modifier,
+    content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
+) {
+    val dragging = index == dragDropState.draggingItemIndex
+    val draggingModifier = if (dragging) {
+        Modifier
+            .zIndex(1f)
+            .graphicsLayer {
+                translationY = dragDropState.draggingItemOffset
+            }
+    } else if (index == dragDropState.previousIndexOfDraggedItem) {
+        Modifier.zIndex(1f)
+            .graphicsLayer {
+                translationY = dragDropState.previousItemOffset.value
+            }
+    } else {
+        Modifier.animateItemPlacement()
+    }
+    Column(modifier = modifier.then(draggingModifier)) {
+        content(dragging)
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 3e5ef66..eca4e16 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -111,6 +111,7 @@
     ComposableDemo("Custom keys") { ReorderWithCustomKeys() },
     ComposableDemo("Fling Config") { LazyWithFlingConfig() },
     ComposableDemo("Item reordering") { PopularBooksDemo() },
+    ComposableDemo("Drag and drop") { LazyColumnDragAndDropDemo() },
     PagingDemos
 )
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
index 92e395d..0d83981 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.AutoTestFrameClock
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
@@ -36,6 +37,11 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertHeightIsAtLeast
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
@@ -866,5 +872,97 @@
             .assertWidthIsEqualTo(columnWidth)
     }
 
+    @Test
+    fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                Modifier.size(itemSize * 3).testTag(LazyGridTag),
+                userScrollEnabled = true,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyGridTag).scrollBy(y = itemSize, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                Modifier.size(itemSize * 3).testTag(LazyGridTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyGridTag).scrollBy(y = itemSize, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+        val itemSizePx = 30f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        lateinit var state: LazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                Modifier.size(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemSizePx)
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                Modifier.size(itemSize * 3).testTag(LazyGridTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyGridTag)
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy))
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollToIndex))
+            // but we still have a read only scroll range property
+            .assert(keyIsDefined(SemanticsProperties.VerticalScrollAxisRange))
+    }
+
     // TODO: add tests for the cache logic
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index fe8ea1e..93cdcc9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -173,6 +173,12 @@
         }
     }
 
+    fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollBy(
+        x = if (vertical) 0.dp else offset,
+        y = if (!vertical) 0.dp else offset,
+        density = rule.density
+    )
+
     @Composable
     fun LazyColumnOrRow(
         modifier: Modifier = Modifier,
@@ -180,6 +186,7 @@
         contentPadding: PaddingValues = PaddingValues(0.dp),
         reverseLayout: Boolean = false,
         flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+        userScrollEnabled: Boolean = true,
         content: LazyListScope.() -> Unit
     ) {
         if (vertical) {
@@ -189,6 +196,7 @@
                 contentPadding = contentPadding,
                 reverseLayout = reverseLayout,
                 flingBehavior = flingBehavior,
+                userScrollEnabled = userScrollEnabled,
                 content = content
             )
         } else {
@@ -198,6 +206,7 @@
                 contentPadding = contentPadding,
                 reverseLayout = reverseLayout,
                 flingBehavior = flingBehavior,
+                userScrollEnabled = userScrollEnabled,
                 content = content
             )
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index 2fa0d60..a676d5a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -50,7 +50,11 @@
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyNotDefined
 import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
@@ -69,7 +73,6 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.collect.Range
@@ -83,8 +86,8 @@
 import java.util.concurrent.CountDownLatch
 
 @LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyListTest() : BaseLazyListTestWithOrientation(Orientation.Vertical) {
+@RunWith(Parameterized::class)
+class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(orientation) {
     private val LazyListTag = "LazyListTag"
     private val firstItemTag = "firstItemTag"
 
@@ -1520,6 +1523,97 @@
         }
     }
 
+    @Test
+    fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+                userScrollEnabled = true,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag).scrollBy(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag).scrollBy(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: LazyListState
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3),
+                state = rememberLazyListState().also { state = it },
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertStartPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+        val itemSize = with(rule.density) { 30.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+                userScrollEnabled = false,
+            ) {
+                items(5) {
+                    Spacer(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .assert(keyNotDefined(SemanticsActions.ScrollBy))
+            .assert(keyNotDefined(SemanticsActions.ScrollToIndex))
+            // but we still have a read only scroll range property
+            .assert(
+                keyIsDefined(
+                    if (vertical) {
+                        SemanticsProperties.VerticalScrollAxisRange
+                    } else {
+                        SemanticsProperties.HorizontalScrollAxisRange
+                    }
+                )
+            )
+    }
+
     // ********************* END OF TESTS *********************
     // Helper functions, etc. live below here
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
index b78cfa9..43c41d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
@@ -183,6 +183,8 @@
  * of them to fill the whole minimum size.
  * @param verticalAlignment the vertical alignment applied to the items
  * @param flingBehavior logic describing fling behavior.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
  * @param content a block which describes the content. Inside this block you can use methods like
  * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
  */
@@ -196,6 +198,7 @@
         if (!reverseLayout) Arrangement.Start else Arrangement.End,
     verticalAlignment: Alignment.Vertical = Alignment.Top,
     flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+    userScrollEnabled: Boolean = true,
     content: LazyListScope.() -> Unit
 ) {
     LazyList(
@@ -207,6 +210,7 @@
         isVertical = false,
         flingBehavior = flingBehavior,
         reverseLayout = reverseLayout,
+        userScrollEnabled = userScrollEnabled,
         content = content
     )
 }
@@ -233,6 +237,8 @@
  * of them to fill the whole minimum size.
  * @param horizontalAlignment the horizontal alignment applied to the items.
  * @param flingBehavior logic describing fling behavior.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
  * @param content a block which describes the content. Inside this block you can use methods like
  * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
  */
@@ -246,6 +252,7 @@
         if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
     flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+    userScrollEnabled: Boolean = true,
     content: LazyListScope.() -> Unit
 ) {
     LazyList(
@@ -257,6 +264,59 @@
         verticalArrangement = verticalArrangement,
         isVertical = true,
         reverseLayout = reverseLayout,
+        userScrollEnabled = userScrollEnabled,
+        content = content
+    )
+}
+
+@Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
+@Composable
+fun LazyColumn(
+    modifier: Modifier = Modifier,
+    state: LazyListState = rememberLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+    content: LazyListScope.() -> Unit
+) {
+    LazyColumn(
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        verticalArrangement = verticalArrangement,
+        horizontalAlignment = horizontalAlignment,
+        flingBehavior = flingBehavior,
+        userScrollEnabled = true,
+        content = content
+    )
+}
+
+@Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
+@Composable
+fun LazyRow(
+    modifier: Modifier = Modifier,
+    state: LazyListState = rememberLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
+    verticalAlignment: Alignment.Vertical = Alignment.Top,
+    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+    content: LazyListScope.() -> Unit
+) {
+    LazyRow(
+        modifier = modifier,
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        horizontalArrangement = horizontalArrangement,
+        verticalAlignment = verticalAlignment,
+        flingBehavior = flingBehavior,
+        userScrollEnabled = true,
         content = content
     )
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index 505a344..608824d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -48,6 +48,8 @@
  * @param contentPadding specify a padding around the whole content
  * @param verticalArrangement The vertical arrangement of the layout's children
  * @param horizontalArrangement The horizontal arrangement of the layout's children
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
  * @param content the [LazyListScope] which describes the content
  */
 @ExperimentalFoundationApi
@@ -59,6 +61,7 @@
     contentPadding: PaddingValues = PaddingValues(0.dp),
     verticalArrangement: Arrangement.Vertical = Arrangement.Top,
     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+    userScrollEnabled: Boolean = true,
     content: LazyGridScope.() -> Unit
 ) {
     when (cells) {
@@ -70,6 +73,7 @@
                 horizontalArrangement = horizontalArrangement,
                 verticalArrangement = verticalArrangement,
                 contentPadding = contentPadding,
+                userScrollEnabled = userScrollEnabled,
                 content = content
             )
         is GridCells.Adaptive ->
@@ -83,6 +87,7 @@
                     horizontalArrangement = horizontalArrangement,
                     verticalArrangement = verticalArrangement,
                     contentPadding = contentPadding,
+                    userScrollEnabled = userScrollEnabled,
                     content = content
                 )
             }
@@ -243,13 +248,15 @@
     contentPadding: PaddingValues,
     verticalArrangement: Arrangement.Vertical,
     horizontalArrangement: Arrangement.Horizontal,
+    userScrollEnabled: Boolean,
     content: LazyGridScope.() -> Unit
 ) {
     LazyColumn(
         modifier = modifier,
         state = state,
         verticalArrangement = verticalArrangement,
-        contentPadding = contentPadding
+        contentPadding = contentPadding,
+        userScrollEnabled = userScrollEnabled
     ) {
         val scope = LazyGridScopeImpl(nColumns)
         scope.apply(content)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
index 0d1af28..ee35422 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazyList.kt
@@ -64,6 +64,8 @@
     isVertical: Boolean,
     /** fling behavior to be used for flinging */
     flingBehavior: FlingBehavior,
+    /** Whether scrolling via the user gestures is allowed. */
+    userScrollEnabled: Boolean,
     /** The alignment to align items horizontally. Required when isVertical is true */
     horizontalAlignment: Alignment.Horizontal? = null,
     /** The vertical arrangement for items. Required when isVertical is true */
@@ -117,7 +119,8 @@
                 state = state,
                 coroutineScope = scope,
                 isVertical = isVertical,
-                reverseScrolling = reverseLayout
+                reverseScrolling = reverseLayout,
+                userScrollEnabled = userScrollEnabled
             )
             .clipScrollableContainer(isVertical)
             .scrollable(
@@ -136,7 +139,8 @@
                 interactionSource = state.internalInteractionSource,
                 flingBehavior = flingBehavior,
                 state = state,
-                overScrollController = overScrollController
+                overScrollController = overScrollController,
+                enabled = userScrollEnabled
             ),
         state = innerState,
         prefetchPolicy = state.prefetchPolicy,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
index 7ecbf46..2a3753c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/list/LazySemantics.kt
@@ -38,7 +38,8 @@
     state: LazyListState,
     coroutineScope: CoroutineScope,
     isVertical: Boolean,
-    reverseScrolling: Boolean
+    reverseScrolling: Boolean,
+    userScrollEnabled: Boolean
 ): Modifier {
     return semantics {
         indexForKey { needle ->
@@ -77,24 +78,30 @@
             horizontalScrollAxisRange = accessibilityScrollState
         }
 
-        scrollBy { x, y ->
-            val delta = if (isVertical) { y } else { x }
-            coroutineScope.launch {
-                (state as ScrollableState).animateScrollBy(delta)
+        if (userScrollEnabled) {
+            scrollBy { x, y ->
+                val delta = if (isVertical) {
+                    y
+                } else {
+                    x
+                }
+                coroutineScope.launch {
+                    (state as ScrollableState).animateScrollBy(delta)
+                }
+                // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+                true
             }
-            // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
-            true
-        }
 
-        scrollToIndex { index ->
-            require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
-                "Can't scroll to index $index, it is out of " +
-                    "bounds [0, ${state.layoutInfo.totalItemsCount})"
+            scrollToIndex { index ->
+                require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+                    "Can't scroll to index $index, it is out of " +
+                        "bounds [0, ${state.layoutInfo.totalItemsCount})"
+                }
+                coroutineScope.launch {
+                    state.scrollToItem(index)
+                }
+                true
             }
-            coroutineScope.launch {
-                state.scrollToItem(index)
-            }
-            true
         }
 
         collectionInfo = CollectionInfo(
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index fb9cc2d..a6917bb 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -479,10 +479,12 @@
 
   public final class ModalBottomSheetKt {
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ModalBottomSheetState sheetState, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.ModalBottomSheetState rememberModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, boolean skipHalfExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.ModalBottomSheetState rememberModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public final class ModalBottomSheetState extends androidx.compose.material.SwipeableState<androidx.compose.material.ModalBottomSheetValue> {
+    ctor public ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, boolean isSkipHalfExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
     ctor public ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? hide(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public boolean isVisible();
@@ -492,7 +494,8 @@
   }
 
   public static final class ModalBottomSheetState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.ModalBottomSheetState,?> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.ModalBottomSheetState,?> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, boolean skipHalfExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
+    method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material.ModalBottomSheetState,?> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmStateChange);
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public enum ModalBottomSheetValue {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
index f754340..18462f3 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
@@ -18,12 +18,16 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.toggleable
 import androidx.compose.material.Button
+import androidx.compose.material.Checkbox
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.Icon
 import androidx.compose.material.ListItem
@@ -34,9 +38,14 @@
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.launch
 
@@ -44,7 +53,11 @@
 @Composable
 @OptIn(ExperimentalMaterialApi::class)
 fun ModalBottomSheetSample() {
-    val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
+    var skipHalfExpanded by remember { mutableStateOf(false) }
+    val state = rememberModalBottomSheetState(
+        initialValue = ModalBottomSheetValue.Hidden,
+        skipHalfExpanded = skipHalfExpanded
+    )
     val scope = rememberCoroutineScope()
     ModalBottomSheetLayout(
         sheetState = state,
@@ -68,11 +81,21 @@
             modifier = Modifier.fillMaxSize().padding(16.dp),
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
-            Text("Rest of the UI")
+            Row(
+                Modifier.toggleable(
+                    value = skipHalfExpanded,
+                    role = Role.Checkbox,
+                    onValueChange = { checked -> skipHalfExpanded = checked }
+                )
+            ) {
+                Checkbox(checked = skipHalfExpanded, onCheckedChange = null)
+                Spacer(Modifier.width(16.dp))
+                Text("Skip Half Expanded State")
+            }
             Spacer(Modifier.height(20.dp))
             Button(onClick = { scope.launch { state.show() } }) {
                 Text("Click to show sheet")
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
new file mode 100644
index 0000000..ca971e5
--- /dev/null
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetStateTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.material
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterialApi::class)
+class ModalBottomSheetStateTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+    private val restorationTester = StateRestorationTester(rule)
+
+    @Test
+    fun test_stateSavedAndRestored() {
+        val initialValue = ModalBottomSheetValue.Hidden
+        val skipHalfExpanded = true
+        val animationSpec = SpringSpec<Float>(visibilityThreshold = 10F)
+        lateinit var state: ModalBottomSheetState
+        restorationTester.setContent {
+            state = rememberModalBottomSheetState(
+                initialValue = initialValue,
+                skipHalfExpanded = skipHalfExpanded,
+                animationSpec = animationSpec
+            )
+        }
+
+        assertThat(state.animationSpec).isEqualTo(animationSpec)
+        assertThat(state.currentValue).isEqualTo(initialValue)
+        assertThat(state.isSkipHalfExpanded).isEqualTo(skipHalfExpanded)
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        assertThat(state.animationSpec).isEqualTo(animationSpec)
+        assertThat(state.currentValue).isEqualTo(initialValue)
+        assertThat(state.isSkipHalfExpanded).isEqualTo(skipHalfExpanded)
+    }
+
+    @Test
+    fun test_halfExpandDisabled_initialValueHalfExpanded_throws() {
+        try {
+            ModalBottomSheetState(
+                initialValue = ModalBottomSheetValue.HalfExpanded,
+                isSkipHalfExpanded = true
+            )
+            fail("ModalBottomSheetState didn't throw an exception")
+        } catch (exception: IllegalArgumentException) {
+            assertThat(exception)
+                .hasMessageThat()
+                .isNotEmpty()
+        }
+    }
+
+    @Test
+    fun test_halfExpandDisabled_initialValueHidden_doesntThrow() {
+        ModalBottomSheetState(
+            initialValue = ModalBottomSheetValue.Hidden,
+            isSkipHalfExpanded = true
+        )
+    }
+
+    @Test
+    fun test_halfExpandDisabled_initialValueExpanded_doesntThrow() {
+        ModalBottomSheetState(
+            initialValue = ModalBottomSheetValue.Expanded,
+            isSkipHalfExpanded = true
+        )
+    }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 46cde1e..6d31620 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -399,6 +399,42 @@
     }
 
     @Test
+    fun modalBottomSheet_showAndHide_manually_skipHalfExpanded(): Unit = runBlocking(
+        AutoTestFrameClock()
+    ) {
+        lateinit var sheetState: ModalBottomSheetState
+        rule.setMaterialContent {
+            sheetState = rememberModalBottomSheetState(
+                ModalBottomSheetValue.Hidden,
+                skipHalfExpanded = true
+            )
+            ModalBottomSheetLayout(
+                sheetState = sheetState,
+                content = {},
+                sheetContent = {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(sheetTag)
+                    )
+                }
+            )
+        }
+
+        assertThat(sheetState.currentValue == ModalBottomSheetValue.Hidden)
+
+        sheetState.show()
+
+        advanceClock()
+
+        assertThat(sheetState.currentValue == ModalBottomSheetValue.Expanded)
+
+        sheetState.hide()
+
+        assertThat(sheetState.currentValue == ModalBottomSheetValue.Hidden)
+    }
+
+    @Test
     fun modalBottomSheet_hideBySwiping() {
         lateinit var sheetState: ModalBottomSheetState
         rule.setMaterialContent {
@@ -415,6 +451,56 @@
                 sheetContent = {
                     Box(
                         Modifier
+                            .fillMaxSize()
+                            .testTag(sheetTag)
+                    )
+                }
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown(endY = rule.rootHeight().toPx() / 2) }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.HalfExpanded)
+        }
+
+        rule.onNodeWithTag(sheetTag)
+            .performTouchInput { swipeDown() }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+        }
+    }
+
+    @Test
+    fun modalBottomSheet_hideBySwiping_skipHalfExpanded() {
+        lateinit var sheetState: ModalBottomSheetState
+        rule.setMaterialContent {
+            sheetState = rememberModalBottomSheetState(
+                ModalBottomSheetValue.Expanded,
+                skipHalfExpanded = true
+            )
+            ModalBottomSheetLayout(
+                sheetState = sheetState,
+                content = {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(contentTag)
+                    )
+                },
+                sheetContent = {
+                    Box(
+                        Modifier
                             .fillMaxWidth()
                             .height(sheetHeight)
                             .testTag(sheetTag)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 759a753..3ba169b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -86,14 +86,22 @@
 /**
  * State of the [ModalBottomSheetLayout] composable.
  *
- * @param initialValue The initial value of the state.
+ * @param initialValue The initial value of the state. <b>Must not be set to
+ * [ModalBottomSheetValue.HalfExpanded] if [isSkipHalfExpanded] is set to true.</b>
  * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param isSkipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b>
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
  */
 @ExperimentalMaterialApi
 class ModalBottomSheetState(
     initialValue: ModalBottomSheetValue,
     animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+    internal val isSkipHalfExpanded: Boolean,
     confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
 ) : SwipeableState<ModalBottomSheetValue>(
     initialValue = initialValue,
@@ -106,19 +114,36 @@
     val isVisible: Boolean
         get() = currentValue != Hidden
 
-    internal val isHalfExpandedEnabled: Boolean
+    internal val hasHalfExpandedState: Boolean
         get() = anchors.values.contains(HalfExpanded)
 
+    constructor(
+        initialValue: ModalBottomSheetValue,
+        animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+        confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+    ) : this(initialValue, animationSpec, isSkipHalfExpanded = false, confirmStateChange)
+
+    init {
+        if (isSkipHalfExpanded) {
+            require(initialValue != HalfExpanded) {
+                "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" +
+                    " true."
+            }
+        }
+    }
+
     /**
-     * Show the bottom sheet with animation and suspend until it's shown. If half expand is
-     * enabled, the bottom sheet will be half expanded. Otherwise it will be fully expanded.
+     * Show the bottom sheet with animation and suspend until it's shown. If the sheet is taller
+     * than 50% of the parent's height, the bottom sheet will be half expanded. Otherwise it will be
+     * fully expanded.
      *
      * @throws [CancellationException] if the animation is interrupted
      */
     suspend fun show() {
-        val targetValue =
-            if (isHalfExpandedEnabled) HalfExpanded
-            else Expanded
+        val targetValue = when {
+            hasHalfExpandedState -> HalfExpanded
+            else -> Expanded
+        }
         animateTo(targetValue = targetValue)
     }
 
@@ -129,7 +154,7 @@
      * @throws [CancellationException] if the animation is interrupted
      */
     internal suspend fun halfExpand() {
-        if (!isHalfExpandedEnabled) {
+        if (!hasHalfExpandedState) {
             return
         }
         animateTo(HalfExpanded)
@@ -159,6 +184,7 @@
          */
         fun Saver(
             animationSpec: AnimationSpec<Float>,
+            skipHalfExpanded: Boolean,
             confirmStateChange: (ModalBottomSheetValue) -> Boolean
         ): Saver<ModalBottomSheetState, *> = Saver(
             save = { it.currentValue },
@@ -166,10 +192,71 @@
                 ModalBottomSheetState(
                     initialValue = it,
                     animationSpec = animationSpec,
+                    isSkipHalfExpanded = skipHalfExpanded,
                     confirmStateChange = confirmStateChange
                 )
             }
         )
+
+        /**
+         * The default [Saver] implementation for [ModalBottomSheetState].
+         */
+        @Deprecated(
+            message = "Please specify the skipHalfExpanded parameter",
+            replaceWith = ReplaceWith(
+                "ModalBottomSheetState.Saver(" +
+                    "animationSpec = animationSpec," +
+                    "skipHalfExpanded = ," +
+                    "confirmStateChange = confirmStateChange" +
+                    ")"
+            )
+        )
+        fun Saver(
+            animationSpec: AnimationSpec<Float>,
+            confirmStateChange: (ModalBottomSheetValue) -> Boolean
+        ): Saver<ModalBottomSheetState, *> = Saver(
+            animationSpec = animationSpec,
+            skipHalfExpanded = false,
+            confirmStateChange = confirmStateChange
+        )
+    }
+}
+
+/**
+ * Create a [ModalBottomSheetState] and [remember] it.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b>
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+@ExperimentalMaterialApi
+fun rememberModalBottomSheetState(
+    initialValue: ModalBottomSheetValue,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+    skipHalfExpanded: Boolean,
+    confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+): ModalBottomSheetState {
+    return rememberSaveable(
+        initialValue, animationSpec, skipHalfExpanded, confirmStateChange,
+        saver = ModalBottomSheetState.Saver(
+            animationSpec = animationSpec,
+            skipHalfExpanded = skipHalfExpanded,
+            confirmStateChange = confirmStateChange
+        )
+    ) {
+        ModalBottomSheetState(
+            initialValue = initialValue,
+            animationSpec = animationSpec,
+            isSkipHalfExpanded = skipHalfExpanded,
+            confirmStateChange = confirmStateChange
+        )
     }
 }
 
@@ -186,20 +273,12 @@
     initialValue: ModalBottomSheetValue,
     animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
-): ModalBottomSheetState {
-    return rememberSaveable(
-        saver = ModalBottomSheetState.Saver(
-            animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange
-        )
-    ) {
-        ModalBottomSheetState(
-            initialValue = initialValue,
-            animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange
-        )
-    }
-}
+): ModalBottomSheetState = rememberModalBottomSheetState(
+    initialValue = initialValue,
+    animationSpec = animationSpec,
+    skipHalfExpanded = false,
+    confirmStateChange = confirmStateChange
+)
 
 /**
  * <a href="https://material.io/components/sheets-bottom#modal-bottom-sheet" class="external" target="_blank">Material Design modal bottom sheet</a>.
@@ -292,7 +371,7 @@
                                 }
                                 true
                             }
-                        } else if (sheetState.isHalfExpandedEnabled) {
+                        } else if (sheetState.hasHalfExpandedState) {
                             collapse {
                                 if (sheetState.confirmStateChange(HalfExpanded)) {
                                     scope.launch { sheetState.halfExpand() }
@@ -321,7 +400,7 @@
 ): Modifier {
     val sheetHeight = sheetHeightState.value
     val modifier = if (sheetHeight != null) {
-        val anchors = if (sheetHeight < fullHeight / 2) {
+        val anchors = if (sheetHeight < fullHeight / 2 || sheetState.isSkipHalfExpanded) {
             mapOf(
                 fullHeight to Hidden,
                 fullHeight - sheetHeight to Expanded
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 1eb7bd7..ecb9fc8 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -253,6 +253,51 @@
   public final class ScaffoldKt {
   }
 
+  @androidx.compose.runtime.Stable public interface SnackbarData {
+    method public void dismiss();
+    method public androidx.compose.material3.SnackbarVisuals getVisuals();
+    method public void performAction();
+    property public abstract androidx.compose.material3.SnackbarVisuals visuals;
+  }
+
+  public enum SnackbarDuration {
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Indefinite;
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Long;
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Short;
+  }
+
+  public final class SnackbarHostKt {
+    method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material3.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SnackbarData,kotlin.Unit> snackbar);
+  }
+
+  @androidx.compose.runtime.Stable public final class SnackbarHostState {
+    ctor public SnackbarHostState();
+    method public androidx.compose.material3.SnackbarData? getCurrentSnackbarData();
+    method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional boolean withDismissAction, optional androidx.compose.material3.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+    property public final androidx.compose.material3.SnackbarData? currentSnackbarData;
+  }
+
+  public final class SnackbarKt {
+    method @androidx.compose.runtime.Composable public static void Snackbar(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissAction, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Snackbar(androidx.compose.material3.SnackbarData snackbarData, optional androidx.compose.ui.Modifier modifier, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional long actionColor);
+  }
+
+  public enum SnackbarResult {
+    enum_constant public static final androidx.compose.material3.SnackbarResult ActionPerformed;
+    enum_constant public static final androidx.compose.material3.SnackbarResult Dismissed;
+  }
+
+  @androidx.compose.runtime.Stable public interface SnackbarVisuals {
+    method public String? getActionLabel();
+    method public androidx.compose.material3.SnackbarDuration getDuration();
+    method public String getMessage();
+    method public boolean getWithDismissAction();
+    property public abstract String? actionLabel;
+    property public abstract androidx.compose.material3.SnackbarDuration duration;
+    property public abstract String message;
+    property public abstract boolean withDismissAction;
+  }
+
   public final class Strings_androidKt {
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 5c90c17..5f8321f 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -305,7 +305,53 @@
   }
 
   public final class ScaffoldKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+  }
+
+  @androidx.compose.runtime.Stable public interface SnackbarData {
+    method public void dismiss();
+    method public androidx.compose.material3.SnackbarVisuals getVisuals();
+    method public void performAction();
+    property public abstract androidx.compose.material3.SnackbarVisuals visuals;
+  }
+
+  public enum SnackbarDuration {
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Indefinite;
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Long;
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Short;
+  }
+
+  public final class SnackbarHostKt {
+    method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material3.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SnackbarData,kotlin.Unit> snackbar);
+  }
+
+  @androidx.compose.runtime.Stable public final class SnackbarHostState {
+    ctor public SnackbarHostState();
+    method public androidx.compose.material3.SnackbarData? getCurrentSnackbarData();
+    method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional boolean withDismissAction, optional androidx.compose.material3.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+    method @androidx.compose.material3.ExperimentalMaterial3Api public suspend Object? showSnackbar(androidx.compose.material3.SnackbarVisuals visuals, kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+    property public final androidx.compose.material3.SnackbarData? currentSnackbarData;
+  }
+
+  public final class SnackbarKt {
+    method @androidx.compose.runtime.Composable public static void Snackbar(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissAction, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Snackbar(androidx.compose.material3.SnackbarData snackbarData, optional androidx.compose.ui.Modifier modifier, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional long actionColor);
+  }
+
+  public enum SnackbarResult {
+    enum_constant public static final androidx.compose.material3.SnackbarResult ActionPerformed;
+    enum_constant public static final androidx.compose.material3.SnackbarResult Dismissed;
+  }
+
+  @androidx.compose.runtime.Stable public interface SnackbarVisuals {
+    method public String? getActionLabel();
+    method public androidx.compose.material3.SnackbarDuration getDuration();
+    method public String getMessage();
+    method public boolean getWithDismissAction();
+    property public abstract String? actionLabel;
+    property public abstract androidx.compose.material3.SnackbarDuration duration;
+    property public abstract String message;
+    property public abstract boolean withDismissAction;
   }
 
   public final class Strings_androidKt {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 1eb7bd7..ecb9fc8 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -253,6 +253,51 @@
   public final class ScaffoldKt {
   }
 
+  @androidx.compose.runtime.Stable public interface SnackbarData {
+    method public void dismiss();
+    method public androidx.compose.material3.SnackbarVisuals getVisuals();
+    method public void performAction();
+    property public abstract androidx.compose.material3.SnackbarVisuals visuals;
+  }
+
+  public enum SnackbarDuration {
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Indefinite;
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Long;
+    enum_constant public static final androidx.compose.material3.SnackbarDuration Short;
+  }
+
+  public final class SnackbarHostKt {
+    method @androidx.compose.runtime.Composable public static void SnackbarHost(androidx.compose.material3.SnackbarHostState hostState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SnackbarData,kotlin.Unit> snackbar);
+  }
+
+  @androidx.compose.runtime.Stable public final class SnackbarHostState {
+    ctor public SnackbarHostState();
+    method public androidx.compose.material3.SnackbarData? getCurrentSnackbarData();
+    method public suspend Object? showSnackbar(String message, optional String? actionLabel, optional boolean withDismissAction, optional androidx.compose.material3.SnackbarDuration duration, optional kotlin.coroutines.Continuation<? super androidx.compose.material3.SnackbarResult> p);
+    property public final androidx.compose.material3.SnackbarData? currentSnackbarData;
+  }
+
+  public final class SnackbarKt {
+    method @androidx.compose.runtime.Composable public static void Snackbar(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissAction, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void Snackbar(androidx.compose.material3.SnackbarData snackbarData, optional androidx.compose.ui.Modifier modifier, optional boolean actionOnNewLine, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional long actionColor);
+  }
+
+  public enum SnackbarResult {
+    enum_constant public static final androidx.compose.material3.SnackbarResult ActionPerformed;
+    enum_constant public static final androidx.compose.material3.SnackbarResult Dismissed;
+  }
+
+  @androidx.compose.runtime.Stable public interface SnackbarVisuals {
+    method public String? getActionLabel();
+    method public androidx.compose.material3.SnackbarDuration getDuration();
+    method public String getMessage();
+    method public boolean getWithDismissAction();
+    property public abstract String? actionLabel;
+    property public abstract androidx.compose.material3.SnackbarDuration duration;
+    property public abstract String message;
+    property public abstract boolean withDismissAction;
+  }
+
   public final class Strings_androidKt {
   }
 
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index baab98a..96529d0 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -169,6 +169,19 @@
     examples = RadioButtonsExamples
 )
 
+private val Snackbars = Component(
+    id = nextId(),
+    name = "Snackbars",
+    description = "Snackbars provide brief messages about app processes at the bottom of the " +
+        "screen.",
+    // No snackbar icon
+    tintIcon = true,
+    guidelinesUrl = "$ComponentGuidelinesUrl/snackbars",
+    docsUrl = "$DocsUrl#snackbar",
+    sourceUrl = "$Material3SourceUrl/Snackbar.kt",
+    examples = SnackbarsExamples
+)
+
 private val TopAppBar = Component(
     id = nextId(),
     name = "Top app bar",
@@ -193,5 +206,6 @@
     NavigationDrawer,
     NavigationRail,
     RadioButtons,
+    Snackbars,
     TopAppBar
 )
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 681d22b..4db8088 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -25,8 +25,8 @@
 import androidx.compose.material3.samples.ButtonWithIconSample
 import androidx.compose.material3.samples.CheckboxSample
 import androidx.compose.material3.samples.ColorSchemeSample
-import androidx.compose.material3.samples.EnterAlwaysSmallTopAppBar
 import androidx.compose.material3.samples.ElevatedButtonSample
+import androidx.compose.material3.samples.EnterAlwaysSmallTopAppBar
 import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
 import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
 import androidx.compose.material3.samples.ExtendedFloatingActionButtonSample
@@ -35,14 +35,18 @@
 import androidx.compose.material3.samples.LargeFloatingActionButtonSample
 import androidx.compose.material3.samples.NavigationBarSample
 import androidx.compose.material3.samples.NavigationBarWithOnlySelectedLabelsSample
+import androidx.compose.material3.samples.NavigationDrawerSample
 import androidx.compose.material3.samples.NavigationRailBottomAlignSample
 import androidx.compose.material3.samples.NavigationRailSample
 import androidx.compose.material3.samples.NavigationRailWithOnlySelectedLabelsSample
 import androidx.compose.material3.samples.OutlinedButtonSample
-import androidx.compose.material3.samples.NavigationDrawerSample
 import androidx.compose.material3.samples.PinnedSmallTopAppBar
 import androidx.compose.material3.samples.RadioButtonSample
 import androidx.compose.material3.samples.RadioGroupSample
+import androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
+import androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+import androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
+import androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
 import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
 import androidx.compose.material3.samples.SimpleSmallTopAppBar
 import androidx.compose.material3.samples.SmallFloatingActionButtonSample
@@ -275,3 +279,36 @@
         RadioGroupSample()
     },
 )
+
+private const val SnackbarsExampleDescription = "Snackbars examples"
+private const val SnackbarsExampleSourceUrl = "$SampleSourceUrl/ScaffoldSamples.kt"
+val SnackbarsExamples = listOf(
+    Example(
+        name = ::ScaffoldWithSimpleSnackbar.name,
+        description = SnackbarsExampleDescription,
+        sourceUrl = SnackbarsExampleSourceUrl
+    ) {
+        ScaffoldWithSimpleSnackbar()
+    },
+    Example(
+        name = ::ScaffoldWithIndefiniteSnackbar.name,
+        description = SnackbarsExampleDescription,
+        sourceUrl = SnackbarsExampleSourceUrl
+    ) {
+        ScaffoldWithIndefiniteSnackbar()
+    },
+    Example(
+        name = ::ScaffoldWithCustomSnackbar.name,
+        description = SnackbarsExampleDescription,
+        sourceUrl = SnackbarsExampleSourceUrl
+    ) {
+        ScaffoldWithCustomSnackbar()
+    },
+    Example(
+        name = ::ScaffoldWithCoroutinesSnackbar.name,
+        description = SnackbarsExampleDescription,
+        sourceUrl = SnackbarsExampleSourceUrl
+    ) {
+        ScaffoldWithCoroutinesSnackbar()
+    }
+)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
index e0eb904..4bd58d6 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
@@ -18,24 +18,47 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExtendedFloatingActionButton
 import androidx.compose.material3.FabPosition
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.SnackbarVisuals
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Sampled
@@ -83,3 +106,189 @@
         }
     )
 }
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithSimpleSnackbar() {
+    val snackbarHostState = remember { SnackbarHostState() }
+    val scope = rememberCoroutineScope()
+    Scaffold(
+        snackbarHost = { SnackbarHost(snackbarHostState) },
+        floatingActionButton = {
+            var clickCount by remember { mutableStateOf(0) }
+            ExtendedFloatingActionButton(
+                text = { Text("Show snackbar") },
+                onClick = {
+                    // show snackbar as a suspend function
+                    scope.launch {
+                        snackbarHostState.showSnackbar(
+                            "Snackbar # ${++clickCount}"
+                        )
+                    }
+                }
+            )
+        },
+        content = { innerPadding ->
+            Text(
+                text = "Body content",
+                modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+            )
+        }
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithIndefiniteSnackbar() {
+    val snackbarHostState = remember { SnackbarHostState() }
+    val scope = rememberCoroutineScope()
+    Scaffold(
+        snackbarHost = { SnackbarHost(snackbarHostState) },
+        floatingActionButton = {
+            var clickCount by remember { mutableStateOf(0) }
+            ExtendedFloatingActionButton(
+                text = { Text("Show snackbar") },
+                onClick = {
+                    // show snackbar as a suspend function
+                    scope.launch {
+                        snackbarHostState.showSnackbar(
+                            message = "Snackbar # ${++clickCount}",
+                            actionLabel = "Action",
+                            withDismissAction = true,
+                            duration = SnackbarDuration.Indefinite
+                        )
+                    }
+                }
+            )
+        },
+        content = { innerPadding ->
+            Text(
+                text = "Body content",
+                modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+            )
+        }
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithCustomSnackbar() {
+    class SnackbarVisualsWithError(
+        override val message: String,
+        val isError: Boolean
+    ) : SnackbarVisuals {
+        override val actionLabel: String
+            get() = if (isError) "Error" else "OK"
+        override val withDismissAction: Boolean
+            get() = false
+        override val duration: SnackbarDuration
+            get() = SnackbarDuration.Long
+    }
+
+    val snackbarHostState = remember { SnackbarHostState() }
+    val scope = rememberCoroutineScope()
+    Scaffold(
+        snackbarHost = {
+            // reuse default SnackbarHost to have default animation and timing handling
+            SnackbarHost(snackbarHostState) { data ->
+                // custom snackbar with the custom action button color and border
+                val isError = (data.visuals as? SnackbarVisualsWithError)?.isError ?: false
+                val buttonColor = if (isError) {
+                    ButtonDefaults.textButtonColors(
+                        containerColor = MaterialTheme.colorScheme.errorContainer,
+                        contentColor = MaterialTheme.colorScheme.error
+                    )
+                } else {
+                    ButtonDefaults.textButtonColors(
+                        contentColor = MaterialTheme.colorScheme.inversePrimary
+                    )
+                }
+
+                Snackbar(
+                    modifier = Modifier
+                        .border(2.dp, MaterialTheme.colorScheme.secondary)
+                        .padding(12.dp),
+                    action = {
+                        TextButton(
+                            onClick = { if (isError) data.dismiss() else data.performAction() },
+                            colors = buttonColor
+                        ) { Text(data.visuals.actionLabel ?: "") }
+                    }
+                ) {
+                    Text(data.visuals.message)
+                }
+            }
+        },
+        floatingActionButton = {
+            var clickCount by remember { mutableStateOf(0) }
+            ExtendedFloatingActionButton(
+                text = { Text("Show snackbar") },
+                onClick = {
+                    scope.launch {
+                        snackbarHostState.showSnackbar(
+                            SnackbarVisualsWithError(
+                                "Snackbar # ${++clickCount}",
+                                isError = clickCount % 2 != 0
+                            )
+                        )
+                    }
+                }
+            )
+        },
+        content = { innerPadding ->
+            Text(
+                text = "Custom Snackbar Demo",
+                modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+            )
+        }
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun ScaffoldWithCoroutinesSnackbar() {
+    // decouple snackbar host state from scaffold state for demo purposes
+    // this state, channel and flow is for demo purposes to demonstrate business logic layer
+    val snackbarHostState = remember { SnackbarHostState() }
+    // we allow only one snackbar to be in the queue here, hence conflated
+    val channel = remember { Channel<Int>(Channel.CONFLATED) }
+    LaunchedEffect(channel) {
+        channel.receiveAsFlow().collect { index ->
+            val result = snackbarHostState.showSnackbar(
+                message = "Snackbar # $index",
+                actionLabel = "Action on $index"
+            )
+            when (result) {
+                SnackbarResult.ActionPerformed -> {
+                    /* action has been performed */
+                }
+                SnackbarResult.Dismissed -> {
+                    /* dismissed, no action needed */
+                }
+            }
+        }
+    }
+    Scaffold(
+        snackbarHost = { SnackbarHost(snackbarHostState) },
+        floatingActionButton = {
+            var clickCount by remember { mutableStateOf(0) }
+            ExtendedFloatingActionButton(
+                text = { Text("Show snackbar") },
+                onClick = {
+                    // offset snackbar data to the business logic
+                    channel.trySend(++clickCount)
+                }
+            )
+        },
+        content = { innerPadding ->
+            Text(
+                "Snackbar demo",
+                modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
+            )
+        }
+    )
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
index 7078717..f6b22f5 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogScreenshotTest.kt
@@ -46,31 +46,29 @@
 
     @Test
     fun alertDialog_lightTheme() {
-        composeTestRule.setContent {
-            MaterialTheme {
-                AlertDialog(
-                    onDismissRequest = {},
-                    title = {
-                        Text(text = "Title")
-                    },
-                    text = {
-                        Text(
-                            "This area typically contains the supportive text " +
-                                "which presents the details regarding the Dialog's purpose."
-                        )
-                    },
-                    confirmButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Confirm")
-                        }
-                    },
-                    dismissButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Dismiss")
-                        }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            AlertDialog(
+                onDismissRequest = {},
+                title = {
+                    Text(text = "Title")
+                },
+                text = {
+                    Text(
+                        "This area typically contains the supportive text " +
+                            "which presents the details regarding the Dialog's purpose."
+                    )
+                },
+                confirmButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Confirm")
                     }
-                )
-            }
+                },
+                dismissButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Dismiss")
+                    }
+                }
+            )
         }
 
         assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_lightTheme")
@@ -78,31 +76,29 @@
 
     @Test
     fun alertDialog_darkTheme() {
-        composeTestRule.setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                AlertDialog(
-                    onDismissRequest = {},
-                    title = {
-                        Text(text = "Title")
-                    },
-                    text = {
-                        Text(
-                            "This area typically contains the supportive text " +
-                                "which presents the details regarding the Dialog's purpose."
-                        )
-                    },
-                    confirmButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Confirm")
-                        }
-                    },
-                    dismissButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Dismiss")
-                        }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            AlertDialog(
+                onDismissRequest = {},
+                title = {
+                    Text(text = "Title")
+                },
+                text = {
+                    Text(
+                        "This area typically contains the supportive text " +
+                            "which presents the details regarding the Dialog's purpose."
+                    )
+                },
+                confirmButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Confirm")
                     }
-                )
-            }
+                },
+                dismissButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Dismiss")
+                    }
+                }
+            )
         }
 
         assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_darkTheme")
@@ -110,32 +106,30 @@
 
     @Test
     fun alertDialog_withIcon_lightTheme() {
-        composeTestRule.setContent {
-            MaterialTheme {
-                AlertDialog(
-                    onDismissRequest = {},
-                    icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
-                    title = {
-                        Text(text = "Title")
-                    },
-                    text = {
-                        Text(
-                            "This area typically contains the supportive text " +
-                                "which presents the details regarding the Dialog's purpose."
-                        )
-                    },
-                    confirmButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Confirm")
-                        }
-                    },
-                    dismissButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Dismiss")
-                        }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            AlertDialog(
+                onDismissRequest = {},
+                icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
+                title = {
+                    Text(text = "Title")
+                },
+                text = {
+                    Text(
+                        "This area typically contains the supportive text " +
+                            "which presents the details regarding the Dialog's purpose."
+                    )
+                },
+                confirmButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Confirm")
                     }
-                )
-            }
+                },
+                dismissButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Dismiss")
+                    }
+                }
+            )
         }
 
         assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_withIcon_lightTheme")
@@ -143,32 +137,30 @@
 
     @Test
     fun alertDialog_withIcon_darkTheme() {
-        composeTestRule.setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                AlertDialog(
-                    onDismissRequest = {},
-                    icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
-                    title = {
-                        Text(text = "Title")
-                    },
-                    text = {
-                        Text(
-                            "This area typically contains the supportive text " +
-                                "which presents the details regarding the Dialog's purpose."
-                        )
-                    },
-                    confirmButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Confirm")
-                        }
-                    },
-                    dismissButton = {
-                        TextButton(onClick = { /* doSomething() */ }) {
-                            Text("Dismiss")
-                        }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            AlertDialog(
+                onDismissRequest = {},
+                icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
+                title = {
+                    Text(text = "Title")
+                },
+                text = {
+                    Text(
+                        "This area typically contains the supportive text " +
+                            "which presents the details regarding the Dialog's purpose."
+                    )
+                },
+                confirmButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Confirm")
                     }
-                )
-            }
+                },
+                dismissButton = {
+                    TextButton(onClick = { /* doSomething() */ }) {
+                        Text("Dismiss")
+                    }
+                }
+            )
         }
 
         assertAppBarAgainstGolden(goldenIdentifier = "alertDialog_withIcon_darkTheme")
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
index 616e103..3221de1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
@@ -193,7 +193,7 @@
 
     @Test
     fun alertDialog_withIcon_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             AlertDialog(
                 onDismissRequest = {},
                 icon = {
@@ -273,7 +273,7 @@
 
     @Test
     fun alertDialog_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             AlertDialog(
                 onDismissRequest = {},
                 title = { Text(text = "Title", modifier = Modifier.testTag(TitleTestTag)) },
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index 215cae5..2dc6046 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -50,31 +50,29 @@
 
     @Test
     fun smallAppBar_lightTheme() {
-        composeTestRule.setContent {
-            MaterialTheme {
-                Box(Modifier.testTag(TestTag)) {
-                    SmallTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                SmallTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -83,31 +81,29 @@
 
     @Test
     fun smallAppBar_darkTheme() {
-        composeTestRule.setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                Box(Modifier.testTag(TestTag)) {
-                    SmallTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                SmallTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -116,31 +112,29 @@
 
     @Test
     fun centerAlignedAppBar_lightTheme() {
-        composeTestRule.setContent {
-            MaterialTheme {
-                Box(Modifier.testTag(TestTag)) {
-                    CenterAlignedTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -149,31 +143,29 @@
 
     @Test
     fun centerAlignedAppBar_darkTheme() {
-        composeTestRule.setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                Box(Modifier.testTag(TestTag)) {
-                    CenterAlignedTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                CenterAlignedTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -182,31 +174,29 @@
 
     @Test
     fun mediumAppBar_lightTheme() {
-        composeTestRule.setContent {
-            MaterialTheme {
-                Box(Modifier.testTag(TestTag)) {
-                    MediumTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                MediumTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -215,31 +205,29 @@
 
     @Test
     fun mediumAppBar_darkTheme() {
-        composeTestRule.setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                Box(Modifier.testTag(TestTag)) {
-                    MediumTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                MediumTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -248,31 +236,29 @@
 
     @Test
     fun largeAppBar_lightTheme() {
-        composeTestRule.setContent {
-            MaterialTheme {
-                Box(Modifier.testTag(TestTag)) {
-                    LargeTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                LargeTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
@@ -281,31 +267,29 @@
 
     @Test
     fun largeAppBar_darkTheme() {
-        composeTestRule.setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                Box(Modifier.testTag(TestTag)) {
-                    LargeTopAppBar(
-                        navigationIcon = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.ArrowBack,
-                                    contentDescription = "Back"
-                                )
-                            }
-                        },
-                        title = {
-                            Text("Title")
-                        },
-                        actions = {
-                            IconButton(onClick = { /* doSomething() */ }) {
-                                Icon(
-                                    imageVector = Icons.Filled.Favorite,
-                                    contentDescription = "Like"
-                                )
-                            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(Modifier.testTag(TestTag)) {
+                LargeTopAppBar(
+                    navigationIcon = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.ArrowBack,
+                                contentDescription = "Back"
+                            )
                         }
-                    )
-                }
+                    },
+                    title = {
+                        Text("Title")
+                    },
+                    actions = {
+                        IconButton(onClick = { /* doSomething() */ }) {
+                            Icon(
+                                imageVector = Icons.Filled.Favorite,
+                                contentDescription = "Like"
+                            )
+                        }
+                    }
+                )
             }
         }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index dd71a66..eb9b182 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -19,6 +19,11 @@
 import android.os.Build
 import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material3.tokens.TopAppBarLarge
 import androidx.compose.material3.tokens.TopAppBarMedium
 import androidx.compose.material3.tokens.TopAppBarSmall
@@ -29,6 +34,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
@@ -47,6 +53,9 @@
 import androidx.compose.ui.test.onLast
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -80,7 +89,7 @@
     @Test
     fun smallTopAppBar_withTitle() {
         val title = "Title"
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 SmallTopAppBar(title = { Text(title) })
             }
@@ -90,7 +99,7 @@
 
     @Test
     fun smallTopAppBar_default_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 SmallTopAppBar(
                     navigationIcon = {
@@ -110,7 +119,7 @@
 
     @Test
     fun smallTopAppBar_noNavigationIcon_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 SmallTopAppBar(
                     title = {
@@ -129,7 +138,7 @@
     fun smallTopAppBar_titleDefaultStyle() {
         var textStyle: TextStyle? = null
         var expectedTextStyle: TextStyle? = null
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             SmallTopAppBar(title = {
                 Text("Title")
                 textStyle = LocalTextStyle.current
@@ -153,7 +162,7 @@
         var expectedActionsColor: Color = Color.Unspecified
         var expectedContainerColor: Color = Color.Unspecified
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             SmallTopAppBar(
                 modifier = Modifier.testTag(TopAppBarTestTag),
                 navigationIcon = {
@@ -204,7 +213,7 @@
         var expectedScrolledContainerColor: Color = Color.Unspecified
         val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             SmallTopAppBar(
                 modifier = Modifier.testTag(TopAppBarTestTag),
                 title = {
@@ -234,7 +243,7 @@
         val scrollOffsetDp = 20.dp
         var scrollOffsetPx = 0f
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             scrollOffsetPx = with(LocalDensity.current) { scrollOffsetDp.toPx() }
             SmallTopAppBar(
                 modifier = Modifier.testTag(TopAppBarTestTag),
@@ -265,7 +274,7 @@
     @Test
     fun centerAlignedTopAppBar_withTitle() {
         val title = "Title"
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 CenterAlignedTopAppBar(title = { Text(title) })
             }
@@ -275,7 +284,7 @@
 
     @Test
     fun centerAlignedTopAppBar_default_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 CenterAlignedTopAppBar(
                     navigationIcon = {
@@ -295,7 +304,7 @@
 
     @Test
     fun centerAlignedTopAppBar_noNavigationIcon_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 CenterAlignedTopAppBar(
                     title = {
@@ -314,7 +323,7 @@
     fun centerAlignedTopAppBar_titleDefaultStyle() {
         var textStyle: TextStyle? = null
         var expectedTextStyle: TextStyle? = null
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             CenterAlignedTopAppBar(
                 title = {
                     Text("Title")
@@ -341,7 +350,7 @@
         var expectedActionsColor: Color = Color.Unspecified
         var expectedContainerColor: Color = Color.Unspecified
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             CenterAlignedTopAppBar(
                 modifier = Modifier.testTag(TopAppBarTestTag),
                 navigationIcon = {
@@ -389,7 +398,7 @@
         var expectedScrolledContainerColor: Color = Color.Unspecified
         val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             CenterAlignedTopAppBar(
                 modifier = Modifier.testTag(TopAppBarTestTag),
                 title = {
@@ -423,7 +432,7 @@
 
     @Test
     fun mediumTopAppBar_expanded_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 MediumTopAppBar(
                     navigationIcon = {
@@ -502,7 +511,7 @@
 
     @Test
     fun largeTopAppBar_expanded_positioning() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(Modifier.testTag(TopAppBarTestTag)) {
                 LargeTopAppBar(
                     navigationIcon = {
@@ -569,6 +578,101 @@
         )
     }
 
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_enterAlways_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(TopAppBarDefaults.enterAlwaysScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_exitUntilCollapsed_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(
+                TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
+                    rememberSplineBasedDecay()
+                ), state
+            )
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_pinned_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(TopAppBarDefaults.pinnedScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Composable
+    private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
+        Scaffold(
+            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+            topBar = {
+                SmallTopAppBar(
+                    modifier = Modifier.testTag(TopAppBarTestTag),
+                    title = { Text(text = "Title") },
+                    scrollBehavior = scrollBehavior,
+                )
+            }
+        ) { contentPadding ->
+            LazyRow(Modifier.fillMaxSize().testTag(LazyListTag), state) {
+                items(2) { page ->
+                    LazyColumn(
+                        modifier = Modifier.fillParentMaxSize(),
+                        contentPadding = contentPadding
+                    ) {
+                        items(50) {
+                            Text(
+                                modifier = Modifier.fillParentMaxWidth(),
+                                text = "Item #$page x $it"
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Checks the app bar's components positioning when it's a [SmallTopAppBar], a
      * [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
@@ -717,7 +821,7 @@
         var partiallyCollapsedOffsetPx = 0f
         var fullyCollapsedOffsetPx = 0f
         var scrollBehavior: TopAppBarScrollBehavior? = null
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             scrollBehavior =
                 TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberSplineBasedDecay())
             with(LocalDensity.current) {
@@ -773,7 +877,7 @@
         var oneThirdCollapsedContainerColor: Color = Color.Unspecified
         var titleContentColor: Color = Color.Unspecified
         var scrollBehavior: TopAppBarScrollBehavior? = null
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             scrollBehavior =
                 TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberSplineBasedDecay())
             // Using the mediumTopAppBarColors for both Medium and Large top app bars, as the
@@ -861,6 +965,7 @@
     private val AppBarStartAndEndPadding = 4.dp
     private val AppBarTopAndBottomPadding = (TopAppBarSmall.SmallContainerHeight - FakeIconSize) / 2
 
+    private val LazyListTag = "lazyList"
     private val TopAppBarTestTag = "bar"
     private val NavigationIconTestTag = "navigationIcon"
     private val TitleTestTag = "title"
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
index dc2f4c2..a70e83b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeScreenshotTest.kt
@@ -53,15 +53,13 @@
 
     @Test
     fun lightTheme_noContent() {
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                Box(
-                    Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
-                    contentAlignment = Alignment.Center
-                ) {
-                    BadgedBox(badge = { Badge() }) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(
+                Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+                contentAlignment = Alignment.Center
+            ) {
+                BadgedBox(badge = { Badge() }) {
+                    Icon(Icons.Filled.Favorite, null)
                 }
             }
         }
@@ -73,15 +71,13 @@
 
     @Test
     fun darkTheme_noContent() {
-        composeTestRule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(
-                    Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
-                    contentAlignment = Alignment.Center
-                ) {
-                    BadgedBox(badge = { Badge() }) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(
+                Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+                contentAlignment = Alignment.Center
+            ) {
+                BadgedBox(badge = { Badge() }) {
+                    Icon(Icons.Filled.Favorite, null)
                 }
             }
         }
@@ -93,15 +89,13 @@
 
     @Test
     fun lightTheme_withContent() {
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                Box(
-                    Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
-                    contentAlignment = Alignment.Center
-                ) {
-                    BadgedBox(badge = { Badge { Text("8") } }) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            Box(
+                Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+                contentAlignment = Alignment.Center
+            ) {
+                BadgedBox(badge = { Badge { Text("8") } }) {
+                    Icon(Icons.Filled.Favorite, null)
                 }
             }
         }
@@ -113,15 +107,13 @@
 
     @Test
     fun darkTheme_withContent() {
-        composeTestRule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(
-                    Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
-                    contentAlignment = Alignment.Center
-                ) {
-                    BadgedBox(badge = { Badge { Text("8") } }) {
-                        Icon(Icons.Filled.Favorite, null)
-                    }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            Box(
+                Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
+                contentAlignment = Alignment.Center
+            ) {
+                BadgedBox(badge = { Badge { Text("8") } }) {
+                    Icon(Icons.Filled.Favorite, null)
                 }
             }
         }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
index 4714915..76b9ac0 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BadgeTest.kt
@@ -104,7 +104,7 @@
     @Test
     fun badge_noContent_shape() {
         var errorColor = Color.Unspecified
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             errorColor = MaterialTheme.colorScheme.fromToken(NavigationBar.BadgeColor)
             Badge(modifier = Modifier.testTag(TestBadgeTag))
         }
@@ -123,7 +123,7 @@
     @Test
     fun badgeBox_noContent_position() {
         rule
-            .setMaterialContent {
+            .setMaterialContent(lightColorScheme()) {
                 BadgedBox(badge = { Badge(Modifier.testTag(TestBadgeTag)) }) {
                     Icon(
                         icon,
@@ -137,8 +137,8 @@
         val badgeBounds = badge.getUnclippedBoundsInRoot()
         badge.assertPositionInRootIsEqualTo(
             expectedLeft =
-                anchorBounds.right + BadgeOffset +
-                    max((NavigationBar.BadgeSize - badgeBounds.width) / 2, 0.dp),
+            anchorBounds.right + BadgeOffset +
+                max((NavigationBar.BadgeSize - badgeBounds.width) / 2, 0.dp),
             expectedTop = -badgeBounds.height / 2
         )
     }
@@ -146,7 +146,7 @@
     @Test
     fun badgeBox_shortContent_position() {
         rule
-            .setMaterialContent {
+            .setMaterialContent(lightColorScheme()) {
                 BadgedBox(badge = { Badge { Text("8") } }) {
                     Icon(
                         icon,
@@ -160,7 +160,7 @@
         val badgeBounds = badge.getUnclippedBoundsInRoot()
         badge.assertPositionInRootIsEqualTo(
             expectedLeft = anchorBounds.right + BadgeWithContentHorizontalOffset + max
-            (
+                (
                 (
                     NavigationBar.LargeBadgeSize - badgeBounds.width
                     ) / 2,
@@ -173,7 +173,7 @@
     @Test
     fun badgeBox_longContent_position() {
         rule
-            .setMaterialContent {
+            .setMaterialContent(lightColorScheme()) {
                 BadgedBox(badge = { Badge { Text("999+") } }) {
                     Icon(
                         icon,
@@ -196,7 +196,7 @@
 
     @Test
     fun badge_notMergingDescendants_withOwnContentDescription() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             BadgedBox(
                 badge = {
                     Badge { Text("99+") }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
index be7cd16..5403fe4 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ButtonTest.kt
@@ -49,7 +49,7 @@
 
     @Test
     fun defaultSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 Button(modifier = Modifier.testTag("myButton"), onClick = {}) {
                     Text("myButton")
@@ -64,7 +64,7 @@
 
     @Test
     fun disabledSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 Button(modifier = Modifier.testTag("myButton"), onClick = {}, enabled = false) {
                     Text("myButton")
@@ -83,7 +83,7 @@
         val onClick: () -> Unit = { ++counter }
         val text = "myButton"
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 Button(onClick = onClick, modifier = Modifier.testTag("myButton")) {
                     Text(text)
@@ -107,7 +107,7 @@
     fun canBeDisabled() {
         val tag = "myButton"
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             var enabled by remember { mutableStateOf(true) }
             val onClick = { enabled = false }
             Box {
@@ -138,7 +138,7 @@
 
         val text = "myButton"
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 Button(modifier = Modifier.testTag(button1Tag), onClick = button1OnClick) {
                     Text(text)
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
index 4dd811e..3d6aa94 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxScreenshotTest.kt
@@ -62,11 +62,9 @@
 
     @Test
     fun checkBox_checked() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(checked = true, onCheckedChange = { })
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(checked = true, onCheckedChange = { })
             }
         }
         assertToggeableAgainstGolden("checkBox_${scheme.name}_checked")
@@ -74,11 +72,9 @@
 
     @Test
     fun checkBox_unchecked() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
             }
         }
         assertToggeableAgainstGolden("checkBox_${scheme.name}_unchecked")
@@ -86,11 +82,9 @@
 
     @Test
     fun checkBox_pressed() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(modifier = wrap, checked = false, onCheckedChange = { })
             }
         }
 
@@ -111,15 +105,13 @@
 
     @Test
     fun checkBox_indeterminate() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    TriStateCheckbox(
-                        state = ToggleableState.Indeterminate,
-                        modifier = wrap,
-                        onClick = {}
-                    )
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                TriStateCheckbox(
+                    state = ToggleableState.Indeterminate,
+                    modifier = wrap,
+                    onClick = {}
+                )
             }
         }
         assertToggeableAgainstGolden("checkBox_${scheme.name}_indeterminate")
@@ -127,15 +119,13 @@
 
     @Test
     fun checkBox_disabled_checked() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(
-                        modifier = wrap,
-                        checked = true,
-                        enabled = false,
-                        onCheckedChange = { })
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap,
+                    checked = true,
+                    enabled = false,
+                    onCheckedChange = { })
             }
         }
         assertToggeableAgainstGolden("checkBox_${scheme.name}_disabled_checked")
@@ -143,15 +133,13 @@
 
     @Test
     fun checkBox_disabled_unchecked() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(
-                        modifier = wrap,
-                        checked = false,
-                        enabled = false,
-                        onCheckedChange = { })
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap,
+                    checked = false,
+                    enabled = false,
+                    onCheckedChange = { })
             }
         }
         assertToggeableAgainstGolden("checkBox_${scheme.name}_disabled_unchecked")
@@ -159,16 +147,14 @@
 
     @Test
     fun checkBox_disabled_indeterminate() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    TriStateCheckbox(
-                        state = ToggleableState.Indeterminate,
-                        enabled = false,
-                        modifier = wrap,
-                        onClick = {}
-                    )
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                TriStateCheckbox(
+                    state = ToggleableState.Indeterminate,
+                    enabled = false,
+                    modifier = wrap,
+                    onClick = {}
+                )
             }
         }
         assertToggeableAgainstGolden("checkBox_${scheme.name}_disabled_indeterminate")
@@ -177,15 +163,13 @@
     @Test
     fun checkBox_unchecked_animateToChecked() {
         val isChecked = mutableStateOf(false)
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(
-                        modifier = wrap,
-                        checked = isChecked.value,
-                        onCheckedChange = { isChecked.value = it }
-                    )
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap,
+                    checked = isChecked.value,
+                    onCheckedChange = { isChecked.value = it }
+                )
             }
         }
 
@@ -208,15 +192,13 @@
     @Test
     fun checkBox_checked_animateToUnchecked() {
         val isChecked = mutableStateOf(true)
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(
-                        modifier = wrap,
-                        checked = isChecked.value,
-                        onCheckedChange = { isChecked.value = it }
-                    )
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap,
+                    checked = isChecked.value,
+                    onCheckedChange = { isChecked.value = it }
+                )
             }
         }
 
@@ -238,15 +220,13 @@
 
     @Test
     fun checkBox_hover() {
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(
-                        modifier = wrap,
-                        checked = true,
-                        onCheckedChange = { }
-                    )
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap,
+                    checked = true,
+                    onCheckedChange = { }
+                )
             }
         }
 
@@ -262,19 +242,17 @@
     fun checkBox_focus() {
         val focusRequester = FocusRequester()
 
-        rule.setContent {
-            MaterialTheme(scheme.colorScheme) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    Checkbox(
-                        modifier = wrap
-                            // Normally this is only focusable in non-touch mode, so let's force it to
-                            // always be focusable so we can test how it appears
-                            .focusProperties { canFocus = true }
-                            .focusRequester(focusRequester),
-                        checked = true,
-                        onCheckedChange = { }
-                    )
-                }
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester),
+                    checked = true,
+                    onCheckedChange = { }
+                )
             }
         }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
index 0ee07c6..d252a0d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CheckboxTest.kt
@@ -28,18 +28,18 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.state.ToggleableState.Indeterminate
 import androidx.compose.ui.state.ToggleableState.Off
 import androidx.compose.ui.state.ToggleableState.On
-import androidx.compose.ui.test.assertHasClickAction
-import androidx.compose.ui.test.assertHasNoClickAction
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHasNoClickAction
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsOff
@@ -72,7 +72,7 @@
 
     @Test
     fun checkBoxTest_defaultSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 Checkbox(false, {}, modifier = Modifier.testTag(tag = "checkboxUnchecked"))
                 Checkbox(true, {}, modifier = Modifier.testTag("checkboxChecked"))
@@ -92,7 +92,7 @@
 
     @Test
     fun checkBoxTest_toggle() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val (checked, onCheckedChange) = remember { mutableStateOf(false) }
             Checkbox(checked, onCheckedChange, modifier = Modifier.testTag(defaultTag))
         }
@@ -105,7 +105,7 @@
 
     @Test
     fun checkBoxTest_toggle_twice() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val (checked, onCheckedChange) = remember { mutableStateOf(false) }
             Checkbox(checked, onCheckedChange, modifier = Modifier.testTag(defaultTag))
         }
@@ -122,7 +122,7 @@
     fun checkBoxTest_untoggleable_whenEmptyLambda() {
         val parentTag = "parent"
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val (checked, _) = remember { mutableStateOf(false) }
             Box(Modifier.semantics(mergeDescendants = true) {}.testTag(parentTag)) {
                 Checkbox(
@@ -144,7 +144,7 @@
 
     @Test
     fun checkBoxTest_untoggleableAndMergeable_whenNullLambda() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val (checked, _) = remember { mutableStateOf(false) }
             Box(Modifier.semantics(mergeDescendants = true) {}.testTag(defaultTag)) {
                 Checkbox(
@@ -281,7 +281,9 @@
                 ) {
                     TriStateCheckbox(
                         state = checkboxValue,
-                        onClick = if (clickable) { {} } else null,
+                        onClick = if (clickable) {
+                            {}
+                        } else null,
                         enabled = false
                     )
                 }
@@ -299,7 +301,7 @@
     fun checkBoxTest_clickInMinimumTouchTarget(): Unit = with(rule.density) {
         val tag = "switch"
         var state by mutableStateOf(Off)
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             // Box is needed because otherwise the control will be expanded to fill its parent
             Box(Modifier.fillMaxSize()) {
                 TriStateCheckbox(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
index 67f6d1e..aac9d78 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -61,7 +61,7 @@
 
     @Test
     fun iconButton_lightTheme() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconButton(onClick = { /* doSomething() */ }) {
                     Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -73,16 +73,14 @@
 
     @Test
     fun iconButton_darkTheme() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Surface(modifier = Modifier.fillMaxSize()) {
-                    Box(wrap.testTag(wrapperTestTag)) {
-                        IconButton(onClick = { /* doSomething() */ }) {
-                            Icon(
-                                Icons.Filled.Favorite,
-                                contentDescription = "Localized description"
-                            )
-                        }
+        rule.setMaterialContent(darkColorScheme()) {
+            Surface(modifier = Modifier.fillMaxSize()) {
+                Box(wrap.testTag(wrapperTestTag)) {
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(
+                            Icons.Filled.Favorite,
+                            contentDescription = "Localized description"
+                        )
                     }
                 }
             }
@@ -92,8 +90,7 @@
 
     @Test
     fun iconButton_lightTheme_disabled() {
-
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconButton(onClick = { /* doSomething() */ }, enabled = false) {
                     Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -105,7 +102,7 @@
 
     @Test
     fun iconButton_lightTheme_pressed() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconButton(onClick = { /* doSomething() */ }) {
                     Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -130,7 +127,7 @@
 
     @Test
     fun iconButton_lightTheme_hovered() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconButton(onClick = { /* doSomething() */ }) {
                     Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
@@ -148,7 +145,7 @@
     fun iconButton_lightTheme_focused() {
         val focusRequester = FocusRequester()
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconButton(onClick = { /* doSomething() */ },
                     modifier = Modifier
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
index 0e7fa45..1454beb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
@@ -95,7 +95,7 @@
 
     @Test
     fun iconButton_defaultSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             IconButtonContent()
         }
         rule.onNode(hasClickAction()).apply {
@@ -105,7 +105,7 @@
 
     @Test
     fun iconButton_disabledSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             IconButton(onClick = {}, enabled = false) {}
         }
         rule.onNode(hasClickAction()).apply {
@@ -116,7 +116,7 @@
     @Test
     fun iconButton_materialIconSize_iconPositioning() {
         val diameter = 24.dp
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 IconButton(onClick = {}) {
                     Box(Modifier.size(diameter).testTag("icon"))
@@ -134,7 +134,7 @@
     fun iconButton_customIconSize_iconPositioning() {
         val width = 36.dp
         val height = 14.dp
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 IconButton(onClick = {}) {
                     Box(Modifier.size(width, height).testTag("icon"))
@@ -177,7 +177,7 @@
 
     @Test
     fun iconToggleButton_defaultSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             IconToggleButtonContent()
         }
         rule.onNode(isToggleable()).apply {
@@ -190,7 +190,7 @@
 
     @Test
     fun iconToggleButton_disabledSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             IconToggleButton(checked = false, onCheckedChange = {}, enabled = false) {}
         }
         rule.onNode(isToggleable()).apply {
@@ -202,7 +202,7 @@
     @Test
     fun iconToggleButton_materialIconSize_iconPositioning() {
         val diameter = 24.dp
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 IconToggleButton(checked = false, onCheckedChange = {}) {
                     Box(Modifier.size(diameter).testTag("icon"))
@@ -220,7 +220,7 @@
     fun iconToggleButton_customIconSize_iconPositioning() {
         val width = 36.dp
         val height = 14.dp
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 IconToggleButton(checked = false, onCheckedChange = {}) {
                     Box(Modifier.size(width, height).testTag("icon"))
@@ -238,7 +238,7 @@
     fun iconToggleButton_clickInMinimumTouchTarget(): Unit = with(rule.density) {
         val tag = "iconToggleButton"
         var checked by mutableStateOf(false)
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             // Box is needed because otherwise the control will be expanded to fill its parent
             Box(Modifier.fillMaxSize()) {
                 IconToggleButton(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
index ef8ae3e..7d7ddf3 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
@@ -26,8 +26,8 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
-import androidx.compose.ui.graphics.painter.ColorPainter
 import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.ColorPainter
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
@@ -159,7 +159,7 @@
         val height = 24.dp
         val testTag = "testTag"
         var expectedIntSize: IntSize? = null
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val image: ImageBitmap
             with(LocalDensity.current) {
                 image = createBitmapWithColor(
@@ -196,7 +196,7 @@
         val width = 35.dp
         val height = 83.dp
         val testTag = "testTag"
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val image: ImageBitmap
             with(LocalDensity.current) {
                 image = createBitmapWithColor(
@@ -219,7 +219,7 @@
         val width = 35.dp
         val height = 83.dp
         val testTag = "testTag"
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val image: ImageBitmap
             with(LocalDensity.current) {
                 image = createBitmapWithColor(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
index 38d2f0c..e1319813e 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MaterialTest.kt
@@ -38,12 +38,20 @@
 import androidx.compose.ui.unit.height
 import androidx.compose.ui.unit.width
 
+/**
+ * Wraps Compose content in a [MaterialTheme] and a [Surface].
+ *
+ * @param colorScheme a [ColorScheme] to provide to the theme. Usually a [lightColorScheme],
+ * [darkColorScheme], or a dynamic one
+ * @param modifier a [Modifier] to be applied at the [Surface] wrapper
+ */
 fun ComposeContentTestRule.setMaterialContent(
+    colorScheme: ColorScheme,
     modifier: Modifier = Modifier,
     composable: @Composable () -> Unit
 ) {
     setContent {
-        MaterialTheme {
+        MaterialTheme(colorScheme = colorScheme) {
             Surface(modifier = modifier, content = composable)
         }
     }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
index 897a894..f181c0f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
@@ -62,11 +62,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationBar(interactionSource)
-            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationBar(interactionSource)
         }
 
         assertNavigationBarMatches(
@@ -83,11 +81,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationBar(interactionSource)
-            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationBar(interactionSource)
         }
 
         assertNavigationBarMatches(
@@ -104,11 +100,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationBar(interactionSource)
-            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationBar(interactionSource)
         }
 
         assertNavigationBarMatches(
@@ -125,11 +119,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationBar(interactionSource)
-            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationBar(interactionSource)
         }
 
         assertNavigationBarMatches(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index b6e78469..f0a898d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -72,7 +72,7 @@
 
     @Test
     fun defaultSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationBar {
                 NavigationBarItem(
                     modifier = Modifier.testTag("item"),
@@ -101,7 +101,7 @@
 
     @Test
     fun disabledSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationBar {
                 NavigationBarItem(
                     enabled = false,
@@ -150,6 +150,7 @@
         lateinit var parentCoords: LayoutCoordinates
         val itemCoords = mutableMapOf<Int, LayoutCoordinates>()
         rule.setMaterialContent(
+            lightColorScheme(),
             Modifier.onGloballyPositioned { coords: LayoutCoordinates ->
                 parentCoords = coords
             }
@@ -190,7 +191,7 @@
 
     @Test
     fun navigationBarItemContent_withLabel_sizeAndPosition() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationBar {
                     NavigationBarItem(
@@ -235,7 +236,7 @@
 
     @Test
     fun navigationBarItemContent_withLabel_unselected_sizeAndPosition() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationBar {
                     NavigationBarItem(
@@ -269,7 +270,7 @@
 
     @Test
     fun navigationBarItemContent_withoutLabel_sizeAndPosition() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationBar {
                     NavigationBarItem(
@@ -297,7 +298,7 @@
 
     @Test
     fun navigationBar_selectNewItem() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             var selectedItem by remember { mutableStateOf(0) }
             val items = listOf("Songs", "Artists", "Playlists")
 
@@ -336,7 +337,7 @@
     @Test
     fun disabled_noClicks() {
         var clicks = 0
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationBar {
                 NavigationBarItem(
                     enabled = false,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt
index a2991a4..d4f5273 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerScreenshotTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.runtime.Composable
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
@@ -52,7 +51,7 @@
     val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
 
     private fun ComposeContentTestRule.setnavigationDrawer(drawerValue: DrawerValue) {
-        setMaterialContent {
+        setMaterialContent(lightColorScheme()) {
             Box(Modifier.requiredSize(400.dp, 32.dp).testTag(ContainerTestTag)) {
                 NavigationDrawer(
                     drawerState = rememberDrawerState(drawerValue),
@@ -67,30 +66,21 @@
         }
     }
 
-    // TODO(b/196872589): Move to MaterialTest
-    private fun ComposeContentTestRule.setMaterialContentWithDarkTheme(
-        modifier: Modifier = Modifier,
-        composable: @Composable () -> Unit,
-    ) {
-        setContent {
-            MaterialTheme(colorScheme = darkColorScheme()) {
-                Surface(modifier = modifier, content = composable)
-            }
-        }
-    }
-
     private fun ComposeContentTestRule.setDarknavigationDrawer(drawerValue: DrawerValue) {
-        setMaterialContentWithDarkTheme {
-            Box(Modifier.requiredSize(400.dp, 32.dp).testTag(ContainerTestTag)) {
-                NavigationDrawer(
-                    drawerState = rememberDrawerState(drawerValue),
-                    drawerContent = {},
-                    content = {
-                        Box(
-                            Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
-                        )
-                    }
-                )
+        setMaterialContent(darkColorScheme()) {
+            Surface {
+                Box(Modifier.requiredSize(400.dp, 32.dp).testTag(ContainerTestTag)) {
+                    NavigationDrawer(
+                        drawerState = rememberDrawerState(drawerValue),
+                        drawerContent = {},
+                        content = {
+                            Box(
+                                Modifier.fillMaxSize()
+                                    .background(MaterialTheme.colorScheme.background)
+                            )
+                        }
+                    )
+                }
             }
         }
     }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
index f2ac639..c65f154 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
@@ -71,7 +71,7 @@
 
     @Test
     fun navigationDrawer_testOffset_whenOpen() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val drawerState = rememberDrawerState(DrawerValue.Open)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -88,7 +88,7 @@
 
     @Test
     fun navigationDrawer_testOffset_whenClosed() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -106,7 +106,7 @@
 
     @Test
     fun navigationDrawer_testWidth_whenOpen() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val drawerState = rememberDrawerState(DrawerValue.Open)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -125,7 +125,7 @@
     @SmallTest
     fun navigationDrawer_hasPaneTitle() {
         lateinit var navigationMenu: String
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationDrawer(
                 drawerState = rememberDrawerState(DrawerValue.Open),
                 drawerContent = {
@@ -145,7 +145,7 @@
     @LargeTest
     fun navigationDrawer_openAndClose(): Unit = runBlocking(AutoTestFrameClock()) {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -176,7 +176,7 @@
     @LargeTest
     fun navigationDrawer_animateTo(): Unit = runBlocking(AutoTestFrameClock()) {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -207,7 +207,7 @@
     @LargeTest
     fun navigationDrawer_snapTo(): Unit = runBlocking(AutoTestFrameClock()) {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -238,7 +238,7 @@
     @LargeTest
     fun navigationDrawer_currentValue(): Unit = runBlocking(AutoTestFrameClock()) {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -269,7 +269,7 @@
         var drawerClicks = 0
         var bodyClicks = 0
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             // emulate click on the screen
             NavigationDrawer(
@@ -310,7 +310,7 @@
     ) {
         var bodyClicks = 0
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -345,7 +345,7 @@
     @LargeTest
     fun navigationDrawer_openBySwipe() {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             Box(Modifier.testTag(DrawerTestTag)) {
                 NavigationDrawer(
@@ -379,7 +379,7 @@
     @LargeTest
     fun navigationDrawer_confirmStateChangeRespect() {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(
                 DrawerValue.Open,
                 confirmStateChange = {
@@ -424,7 +424,7 @@
     @LargeTest
     fun navigationDrawer_openBySwipe_rtl() {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             // emulate click on the screen
             CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
@@ -463,7 +463,7 @@
         AutoTestFrameClock()
     ) {
         lateinit var drawerState: DrawerState
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             drawerState = rememberDrawerState(DrawerValue.Closed)
             NavigationDrawer(
                 drawerState = drawerState,
@@ -500,7 +500,7 @@
     fun navigationDrawer_scrimNode_reportToSemanticsWhenOpen_notReportToSemanticsWhenClosed() {
         val topTag = "navigationDrawer"
         lateinit var closeDrawer: String
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationDrawer(
                 modifier = Modifier.testTag(topTag),
                 drawerState = rememberDrawerState(DrawerValue.Open),
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
index 7fe8197..3395ae0 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailScreenshotTest.kt
@@ -64,11 +64,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationRail(interactionSource)
-            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationRail(interactionSource)
         }
 
         assertNavigationRailMatches(
@@ -85,11 +83,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationRail(interactionSource)
-            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationRail(interactionSource)
         }
 
         assertNavigationRailMatches(
@@ -106,11 +102,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationRail(interactionSource)
-            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationRail(interactionSource)
         }
 
         assertNavigationRailMatches(
@@ -127,11 +121,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationRail(interactionSource)
-            }
+        composeTestRule.setMaterialContent(darkColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationRail(interactionSource)
         }
 
         assertNavigationRailMatches(
@@ -148,11 +140,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationRail(interactionSource, withHeaderFab = true)
-            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationRail(interactionSource, withHeaderFab = true)
         }
 
         assertNavigationRailMatches(
@@ -169,11 +159,9 @@
 
         var scope: CoroutineScope? = null
 
-        composeTestRule.setContent {
-            MaterialTheme(lightColorScheme()) {
-                scope = rememberCoroutineScope()
-                DefaultNavigationRail(interactionSource, withHeaderFab = true)
-            }
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationRail(interactionSource, withHeaderFab = true)
         }
 
         assertNavigationRailMatches(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
index e1ed22a..99b4032 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationRailTest.kt
@@ -74,7 +74,7 @@
 
     @Test
     fun defaultSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationRail {
                 NavigationRailItem(
                     modifier = Modifier.testTag("item"),
@@ -103,7 +103,7 @@
 
     @Test
     fun disabledSemantics() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationRail {
                 NavigationRailItem(
                     enabled = false,
@@ -149,7 +149,7 @@
     @Test
     fun navigationRailItem_sizeAndPositions() {
         val itemCoords = mutableMapOf<Int, LayoutCoordinates>()
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationRail {
                     repeat(4) { index ->
@@ -194,7 +194,7 @@
 
     @Test
     fun navigationRailItemContent_withLabel_sizeAndPosition() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationRail {
                     NavigationRailItem(
@@ -237,7 +237,7 @@
 
     @Test
     fun navigationRailItemContent_withLabel_unselected_sizeAndPosition() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationRail {
                     NavigationRailItem(
@@ -274,7 +274,7 @@
 
     @Test
     fun navigationRailItemContent_withoutLabel_sizeAndPosition() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box {
                 NavigationRail {
                     NavigationRailItem(
@@ -305,7 +305,7 @@
 
     @Test
     fun navigationRail_selectNewItem() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             var selectedItem by remember { mutableStateOf(0) }
             val items = listOf("Home", "Search", "Settings")
             val icons = listOf(Icons.Filled.Home, Icons.Filled.Search, Icons.Filled.Settings)
@@ -344,7 +344,7 @@
     @Test
     fun disabled_noClicks() {
         var clicks = 0
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             NavigationRail {
                 NavigationRailItem(
                     enabled = false,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
index 2bb97e1..06f365b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonScreenshotTest.kt
@@ -60,7 +60,7 @@
 
     @Test
     fun radioButton_lightTheme_selected() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(selected = true, onClick = {})
             }
@@ -70,11 +70,9 @@
 
     @Test
     fun radioButton_darkTheme_selected() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(selected = true, onClick = {})
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = true, onClick = {})
             }
         }
         assertSelectableAgainstGolden("radioButton_darkTheme_selected")
@@ -82,7 +80,7 @@
 
     @Test
     fun radioButton_lightTheme_notSelected() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(selected = false, onClick = {})
             }
@@ -92,11 +90,9 @@
 
     @Test
     fun radioButton_darkTheme_notSelected() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(selected = false, onClick = {})
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = false, onClick = {})
             }
         }
         assertSelectableAgainstGolden("radioButton_darkTheme_notSelected")
@@ -104,7 +100,7 @@
 
     @Test
     fun radioButton_lightTheme_pressed() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(selected = false, onClick = {})
             }
@@ -127,11 +123,9 @@
 
     @Test
     fun radioButton_darkTheme_pressed() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(selected = false, onClick = {})
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = false, onClick = {})
             }
         }
 
@@ -152,7 +146,7 @@
 
     @Test
     fun radioButton_lightTheme_hovered() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(selected = false, onClick = {})
             }
@@ -166,11 +160,9 @@
 
     @Test
     fun radioButton_darkTheme_hovered() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(selected = false, onClick = {})
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = false, onClick = {})
             }
         }
         rule.onNodeWithTag(wrapperTestTag).performMouseInput {
@@ -184,7 +176,7 @@
     fun radioButton_lightTheme_focused() {
         val focusRequester = FocusRequester()
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(
                     selected = false,
@@ -209,19 +201,17 @@
     fun radioButton_darkTheme_focused() {
         val focusRequester = FocusRequester()
 
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(
-                        selected = false,
-                        onClick = {},
-                        modifier = Modifier
-                            // Normally this is only focusable in non-touch mode, so let's force it to
-                            // always be focusable so we can test how it appears
-                            .focusProperties { canFocus = true }
-                            .focusRequester(focusRequester)
-                    )
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(
+                    selected = false,
+                    onClick = {},
+                    modifier = Modifier
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester)
+                )
             }
         }
 
@@ -234,7 +224,7 @@
 
     @Test
     fun radioButton_lightTheme_disabled_selected() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(selected = true, onClick = {}, enabled = false)
             }
@@ -244,11 +234,9 @@
 
     @Test
     fun radioButton_darkTheme_disabled_selected() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(selected = true, onClick = {}, enabled = false)
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = true, onClick = {}, enabled = false)
             }
         }
         assertSelectableAgainstGolden("radioButton_darkTheme_disabled_selected")
@@ -256,7 +244,7 @@
 
     @Test
     fun radioButton_lightTheme_disabled_notSelected() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(selected = false, onClick = {}, enabled = false)
             }
@@ -266,11 +254,9 @@
 
     @Test
     fun radioButton_darkTheme_disabled_notSelected() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(selected = false, onClick = {}, enabled = false)
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(selected = false, onClick = {}, enabled = false)
             }
         }
         assertSelectableAgainstGolden("radioButton_darkTheme_disabled_notSelected")
@@ -278,7 +264,7 @@
 
     @Test
     fun radioButton_lightTheme_notSelected_animateToSelected() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val isSelected = remember { mutableStateOf(false) }
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(
@@ -308,15 +294,13 @@
 
     @Test
     fun radioButton_darkTheme_notSelected_animateToSelected() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                val isSelected = remember { mutableStateOf(false) }
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(
-                        selected = isSelected.value,
-                        onClick = { isSelected.value = !isSelected.value }
-                    )
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            val isSelected = remember { mutableStateOf(false) }
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(
+                    selected = isSelected.value,
+                    onClick = { isSelected.value = !isSelected.value }
+                )
             }
         }
 
@@ -340,7 +324,7 @@
 
     @Test
     fun radioButton_lightTheme_selected_animateToNotSelected() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             val isSelected = remember { mutableStateOf(true) }
             Box(wrap.testTag(wrapperTestTag)) {
                 RadioButton(
@@ -370,15 +354,13 @@
 
     @Test
     fun radioButton_darkTheme_selected_animateToNotSelected() {
-        rule.setContent {
-            MaterialTheme(darkColorScheme()) {
-                val isSelected = remember { mutableStateOf(true) }
-                Box(wrap.testTag(wrapperTestTag)) {
-                    RadioButton(
-                        selected = isSelected.value,
-                        onClick = { isSelected.value = !isSelected.value }
-                    )
-                }
+        rule.setMaterialContent(darkColorScheme()) {
+            val isSelected = remember { mutableStateOf(true) }
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(
+                    selected = isSelected.value,
+                    onClick = { isSelected.value = !isSelected.value }
+                )
             }
         }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
index 5c00290c..a5909aa 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/RadioButtonTest.kt
@@ -66,7 +66,7 @@
     fun radioGroupTest_defaultSemantics() {
         val selected = mutableStateOf(itemOne)
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 options.forEach { item ->
                     RadioButton(
@@ -99,7 +99,7 @@
     fun radioGroupTest_ensureUnselectable() {
         val selected = mutableStateOf(itemOne)
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 options.forEach { item ->
                     RadioButton(
@@ -126,7 +126,7 @@
     @Test
     fun radioGroupTest_clickSelect() {
         val selected = mutableStateOf(itemOne)
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 options.forEach { item ->
                     RadioButton(
@@ -152,7 +152,7 @@
     @Test
     fun radioGroup_untoggleableAndMergeable_whenNullLambda() {
         val parentTag = "parent"
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column(Modifier.semantics(mergeDescendants = true) {}.testTag(parentTag)) {
                 RadioButton(
                     selected = true,
@@ -172,7 +172,7 @@
     fun radioGroupTest_clickSelectTwoDifferentItems() {
         val selected = mutableStateOf(itemOne)
 
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 options.forEach { item ->
                     RadioButton(
@@ -287,7 +287,10 @@
                 CompositionLocalProvider(
                     LocalMinimumTouchTargetEnforcement provides minimumTouchTarget
                 ) {
-                    RadioButton(selected = selected, onClick = if (clickable) { {} } else null)
+                    RadioButton(
+                        selected = selected, onClick = if (clickable) {
+                            {}
+                        } else null)
                 }
             }
             .run {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
index 1b7f6f7..1eb38c1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -77,7 +77,7 @@
     fun scaffold_onlyContent_stackSlot() {
         var child1: Offset = Offset.Zero
         var child2: Offset = Offset.Zero
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Scaffold {
                 Text(
                     "One",
@@ -98,7 +98,7 @@
         var appbarPosition: Offset = Offset.Zero
         var appbarSize: IntSize = IntSize.Zero
         var contentPosition: Offset = Offset.Zero
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Scaffold(
                 topBar = {
                     Box(
@@ -132,7 +132,7 @@
         var appbarSize: IntSize = IntSize.Zero
         var contentPosition: Offset = Offset.Zero
         var contentSize: IntSize = IntSize.Zero
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Scaffold(
                 bottomBar = {
                     Box(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
new file mode 100644
index 0000000..1b1df83b
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.material3
+
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.AccessibilityManager
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.doReturn
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.AdditionalMatchers.not
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SnackbarHostTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun snackbarHost_observePushedData() {
+        var resultedInvocation = ""
+        val hostState = SnackbarHostState()
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            SnackbarHost(hostState) { data ->
+                LaunchedEffect(data) {
+                    resultedInvocation += data.visuals.message
+                    data.dismiss()
+                }
+            }
+        }
+        val job = scope.launch {
+            hostState.showSnackbar("1")
+            Truth.assertThat(resultedInvocation).isEqualTo("1")
+            hostState.showSnackbar("2")
+            Truth.assertThat(resultedInvocation).isEqualTo("12")
+            hostState.showSnackbar("3")
+            Truth.assertThat(resultedInvocation).isEqualTo("123")
+        }
+
+        rule.waitUntil { job.isCompleted }
+    }
+
+    @Test
+    fun snackbarHost_fifoQueueContract() {
+        var resultedInvocation = ""
+        val hostState = SnackbarHostState()
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            SnackbarHost(hostState) { data ->
+                LaunchedEffect(data) {
+                    resultedInvocation += data.visuals.message
+                    launch {
+                        delay(30L)
+                        data.dismiss()
+                    }
+                }
+            }
+        }
+        val parent = SupervisorJob()
+        repeat(10) {
+            scope.launch(parent) {
+                delay(it * 10L)
+                hostState.showSnackbar(it.toString())
+            }
+        }
+
+        rule.waitUntil { parent.children.all { it.isCompleted } }
+        Truth.assertThat(resultedInvocation).isEqualTo("0123456789")
+    }
+
+    @Test
+    @LargeTest
+    fun snackbarHost_returnedResult() {
+        val hostState = SnackbarHostState()
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            SnackbarHost(hostState) { data ->
+                Snackbar(data)
+            }
+        }
+        val job1 = scope.launch {
+            val result = hostState.showSnackbar("1", actionLabel = "press")
+            Truth.assertThat(result).isEqualTo(SnackbarResult.ActionPerformed)
+        }
+        rule.onNodeWithText("press")
+            .performClick()
+
+        rule.waitUntil { job1.isCompleted }
+
+        val job2 = scope.launch {
+            val result = hostState.showSnackbar(
+                message = "1",
+                actionLabel = "do not press"
+            )
+            Truth.assertThat(result).isEqualTo(SnackbarResult.Dismissed)
+        }
+
+        rule.waitUntil(timeoutMillis = 5_000) { job2.isCompleted }
+    }
+
+    @Test
+    fun snackbarHost_scopeLifecycleRespect() {
+        val switchState = mutableStateOf(true)
+        val hostState = SnackbarHostState()
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            if (switchState.value) {
+                scope = rememberCoroutineScope()
+            }
+            SnackbarHost(hostState) { data ->
+                Snackbar(data)
+            }
+        }
+        val job1 = scope.launch {
+            hostState.showSnackbar("1")
+            Truth.assertWithMessage("Result shouldn't happen due to cancellation").fail()
+        }
+        val job2 = scope.launch {
+            delay(10)
+            switchState.value = false
+        }
+
+        rule.waitUntil { job1.isCompleted && job2.isCompleted }
+    }
+
+    @Test
+    fun snackbarHost_semantics() {
+        val hostState = SnackbarHostState()
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            SnackbarHost(hostState) { data ->
+                Snackbar(data)
+            }
+        }
+        val job1 = scope.launch {
+            val result = hostState.showSnackbar("1", actionLabel = "press")
+            Truth.assertThat(result).isEqualTo(SnackbarResult.Dismissed)
+        }
+        rule.onNodeWithText("1").onParent().onParent()
+            .assert(
+                SemanticsMatcher.expectValue(SemanticsProperties.LiveRegion, LiveRegionMode.Polite)
+            )
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+            .performSemanticsAction(SemanticsActions.Dismiss)
+
+        rule.waitUntil { job1.isCompleted }
+    }
+
+    @Test
+    fun snackbarDuration_toMillis_nonNullAccessibilityManager() {
+        val mockDurationControl = 10000L
+        val mockDurationNonControl = 5000L
+        val accessibilityManager: AccessibilityManager = mock {
+            on {
+                calculateRecommendedTimeoutMillis(eq(Long.MAX_VALUE), any(), any(), any())
+            } doReturn Long.MAX_VALUE
+            on {
+                calculateRecommendedTimeoutMillis(not(eq(Long.MAX_VALUE)), any(), any(), eq(true))
+            } doReturn mockDurationControl
+            on {
+                calculateRecommendedTimeoutMillis(not(eq(Long.MAX_VALUE)), any(), any(), eq(false))
+            } doReturn mockDurationNonControl
+        }
+        assertEquals(
+            Long.MAX_VALUE,
+            SnackbarDuration.Indefinite.toMillis(true, accessibilityManager)
+        )
+        assertEquals(
+            Long.MAX_VALUE,
+            SnackbarDuration.Indefinite.toMillis(false, accessibilityManager)
+        )
+        assertEquals(
+            mockDurationControl,
+            SnackbarDuration.Long.toMillis(true, accessibilityManager)
+        )
+        assertEquals(
+            mockDurationNonControl,
+            SnackbarDuration.Long.toMillis(false, accessibilityManager)
+        )
+        assertEquals(
+            mockDurationControl,
+            SnackbarDuration.Short.toMillis(true, accessibilityManager)
+        )
+        assertEquals(
+            mockDurationNonControl,
+            SnackbarDuration.Short.toMillis(false, accessibilityManager)
+        )
+    }
+
+    @Test
+    fun snackbarDuration_toMillis_nullAccessibilityManager() {
+        assertEquals(
+            Long.MAX_VALUE,
+            SnackbarDuration.Indefinite.toMillis(true, null)
+        )
+        assertEquals(
+            Long.MAX_VALUE,
+            SnackbarDuration.Indefinite.toMillis(false, null)
+        )
+        assertEquals(
+            10000L,
+            SnackbarDuration.Long.toMillis(true, null)
+        )
+        assertEquals(
+            10000L,
+            SnackbarDuration.Long.toMillis(false, null)
+        )
+        assertEquals(
+            4000L,
+            SnackbarDuration.Short.toMillis(true, null)
+        )
+        assertEquals(
+            4000L,
+            SnackbarDuration.Short.toMillis(false, null)
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
new file mode 100644
index 0000000..893ac64
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarScreenshotTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.material3
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTestApi::class)
+class SnackbarScreenshotTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    private val snackbarTestTag = "snackbarTestTag"
+
+    @Test
+    fun snackbar_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            TestSnackbar()
+        }
+        assertAgainstGolden("snackbar_lightTheme")
+    }
+
+    @Test
+    fun snackbar_withAction_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            TestSnackbar(showAction = true)
+        }
+        assertAgainstGolden("snackbar_withAction_lightTheme")
+    }
+
+    @Test
+    fun snackbar_withDismiss_lightTheme() {
+        rule.setMaterialContent(lightColorScheme()) {
+            TestSnackbar(showAction = true, duration = SnackbarDuration.Indefinite)
+        }
+        assertAgainstGolden("snackbar_withDismiss_lightTheme")
+    }
+
+    @Test
+    fun snackbar_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            TestSnackbar()
+        }
+        assertAgainstGolden("snackbar_darkTheme")
+    }
+
+    @Test
+    fun snackbar_withAction_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            TestSnackbar(showAction = true)
+        }
+        assertAgainstGolden("snackbar_withAction_darkTheme")
+    }
+
+    @Test
+    fun snackbar_withDismiss_darkTheme() {
+        rule.setMaterialContent(darkColorScheme()) {
+            TestSnackbar(showAction = true, duration = SnackbarDuration.Indefinite)
+        }
+        assertAgainstGolden("snackbar_withDismiss_darkTheme")
+    }
+
+    @Composable
+    private fun TestSnackbar(
+        showAction: Boolean = false,
+        duration: SnackbarDuration = SnackbarDuration.Long
+    ) {
+        Snackbar(
+            snackbarData = object : SnackbarData {
+                override val visuals: SnackbarVisuals = object : SnackbarVisuals {
+                    override val message: String = "Snackbar message"
+                    override val actionLabel: String? = if (showAction) "Undo" else null
+                    override val withDismissAction: Boolean =
+                        duration == SnackbarDuration.Indefinite
+                    override val duration: SnackbarDuration = duration
+                }
+
+                override fun performAction() {
+                    // no-op
+                }
+
+                override fun dismiss() {
+                    // no-op
+                }
+            },
+            modifier = Modifier.testTag(snackbarTestTag),
+        )
+    }
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(snackbarTestTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
new file mode 100644
index 0000000..baca7ca
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.testutils.assertIsNotEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getAlignmentLinePosition
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.max
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SnackbarTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val longText = "Message is very long and long and long and long and long " +
+        "and long and long and long and long and long and long"
+
+    @Test
+    fun defaultSnackbar_semantics() {
+        var clicked = false
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                Snackbar(
+                    content = { Text("Message") },
+                    action = {
+                        TextButton(onClick = { clicked = true }) {
+                            Text("UNDO")
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithText("Message")
+            .assertExists()
+
+        assertThat(clicked).isFalse()
+
+        rule.onNodeWithText("UNDO")
+            .performClick()
+
+        assertThat(clicked).isTrue()
+    }
+
+    @Test
+    fun snackbar_shortTextOnly_defaultSizes() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp
+        ) {
+            Snackbar(
+                content = {
+                    Text("Message")
+                }
+            )
+        }
+            .assertWidthIsEqualTo(300.dp)
+            .assertHeightIsEqualTo(48.dp)
+
+        val firstBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+        val lastBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(LastBaseline)
+        firstBaseLine.assertIsNotEqualTo(0.dp, "first baseline")
+        firstBaseLine.assertIsEqualTo(lastBaseLine, "first baseline")
+
+        val snackBounds = snackbar.getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+
+        val textTopOffset = textBounds.top - snackBounds.top
+        val textBottomOffset = textBounds.top - snackBounds.top
+
+        textTopOffset.assertIsEqualTo(textBottomOffset)
+    }
+
+    @Test
+    fun snackbar_shortTextOnly_bigFont_centered() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp
+        ) {
+            Snackbar(
+                content = {
+                    Text("Message", fontSize = 30.sp)
+                }
+            )
+        }
+            .assertWidthIsEqualTo(300.dp)
+
+        val firstBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+        val lastBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(LastBaseline)
+        firstBaseLine.assertIsNotEqualTo(0.dp, "first baseline")
+        firstBaseLine.assertIsEqualTo(lastBaseLine, "first baseline")
+
+        val snackBounds = snackbar.getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+
+        val textTopOffset = textBounds.top - snackBounds.top
+        val textBottomOffset = textBounds.top - snackBounds.top
+
+        textTopOffset.assertIsEqualTo(textBottomOffset)
+    }
+
+    @Test
+    fun snackbar_shortTextAndButton_alignment() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp
+        ) {
+            Snackbar(
+                content = {
+                    Text("Message")
+                },
+                action = {
+                    TextButton(
+                        onClick = {},
+                        modifier = Modifier.clipToBounds().testTag("button")
+                    ) {
+                        Text("Undo")
+                    }
+                }
+            )
+        }
+            .assertWidthIsEqualTo(300.dp)
+            .assertHeightIsEqualTo(48.dp)
+
+        val textBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+        val buttonBaseLine = rule.onNodeWithTag("button").getAlignmentLinePosition(FirstBaseline)
+        textBaseLine.assertIsNotEqualTo(0.dp, "text baseline")
+        buttonBaseLine.assertIsNotEqualTo(0.dp, "button baseline")
+
+        val snackBounds = snackbar.getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+        val buttonBounds = rule.onNodeWithText("Undo").getBoundsInRoot()
+
+        val buttonTopOffset = buttonBounds.top - snackBounds.top
+        val textTopOffset = textBounds.top - snackBounds.top
+        val textBottomOffset = textBounds.top - snackBounds.top
+        textTopOffset.assertIsEqualTo(textBottomOffset)
+
+        (buttonBaseLine + buttonTopOffset).assertIsEqualTo(textBaseLine + textTopOffset)
+    }
+
+    @Test
+    fun snackbar_shortTextAndButton_bigFont_alignment() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 400.dp
+        ) {
+            val fontSize = 30.sp
+            Snackbar(
+                content = {
+                    Text("Message", fontSize = fontSize)
+                },
+                action = {
+                    TextButton(
+                        onClick = {},
+                        modifier = Modifier.testTag("button")
+                    ) {
+                        Text("Undo", fontSize = fontSize)
+                    }
+                }
+            )
+        }
+
+        val textBaseLine = rule.onNodeWithText("Message").getAlignmentLinePosition(FirstBaseline)
+        val buttonBaseLine = rule.onNodeWithTag("button").getAlignmentLinePosition(FirstBaseline)
+        textBaseLine.assertIsNotEqualTo(0.dp, "text baseline")
+        buttonBaseLine.assertIsNotEqualTo(0.dp, "button baseline")
+
+        val snackBounds = snackbar.getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+        val buttonBounds = rule.onNodeWithText("Undo").getUnclippedBoundsInRoot()
+
+        val buttonTopOffset = buttonBounds.top - snackBounds.top
+        val textTopOffset = textBounds.top - snackBounds.top
+        val textBottomOffset = textBounds.top - snackBounds.top
+        textTopOffset.assertIsEqualTo(textBottomOffset)
+
+        (buttonBaseLine + buttonTopOffset).assertIsEqualTo(textBaseLine + textTopOffset)
+    }
+
+    @Test
+    fun snackbar_longText_sizes() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp
+        ) {
+            Snackbar(
+                content = {
+                    Text(longText, Modifier.testTag("text"), maxLines = 2)
+                }
+            )
+        }
+            .assertWidthIsEqualTo(300.dp)
+            .assertHeightIsEqualTo(68.dp)
+
+        val firstBaseline = rule.onNodeWithTag("text").getFirstBaselinePosition()
+        val lastBaseline = rule.onNodeWithTag("text").getLastBaselinePosition()
+
+        firstBaseline.assertIsNotEqualTo(0.dp, "first baseline")
+        lastBaseline.assertIsNotEqualTo(0.dp, "last baseline")
+        firstBaseline.assertIsNotEqualTo(lastBaseline, "first baseline")
+
+        val snackBounds = snackbar.getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithTag("text").getUnclippedBoundsInRoot()
+
+        val textTopOffset = textBounds.top - snackBounds.top
+        val textBottomOffset = textBounds.top - snackBounds.top
+
+        textTopOffset.assertIsEqualTo(textBottomOffset)
+    }
+
+    @Test
+    fun snackbar_longTextAndButton_alignment() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp
+        ) {
+            Snackbar(
+                content = {
+                    Text(longText, Modifier.testTag("text"), maxLines = 2)
+                },
+                action = {
+                    TextButton(
+                        modifier = Modifier.testTag("button"),
+                        onClick = {}
+                    ) {
+                        Text("Undo")
+                    }
+                }
+            )
+        }
+            .assertWidthIsEqualTo(300.dp)
+            .assertHeightIsEqualTo(68.dp)
+
+        val textFirstBaseLine = rule.onNodeWithTag("text").getFirstBaselinePosition()
+        val textLastBaseLine = rule.onNodeWithTag("text").getLastBaselinePosition()
+
+        textFirstBaseLine.assertIsNotEqualTo(0.dp, "first baseline")
+        textLastBaseLine.assertIsNotEqualTo(0.dp, "last baseline")
+        textFirstBaseLine.assertIsNotEqualTo(textLastBaseLine, "first baseline")
+
+        rule.onNodeWithTag("text")
+            .assertTopPositionInRootIsEqualTo(30.dp - textFirstBaseLine)
+
+        val buttonBounds = rule.onNodeWithTag("button").getUnclippedBoundsInRoot()
+        val snackBounds = snackbar.getUnclippedBoundsInRoot()
+
+        val buttonCenter = buttonBounds.top + (buttonBounds.height / 2)
+        buttonCenter.assertIsEqualTo(snackBounds.height / 2, "button center")
+    }
+
+    @Test
+    fun snackbar_textAndButtonOnSeparateLine_alignment() {
+        val snackbar = rule.setMaterialContentForSizeAssertions(
+            parentMaxWidth = 300.dp
+        ) {
+            Snackbar(
+                content = {
+                    Text("Message", Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp))
+                },
+                action = {
+                    TextButton(
+                        onClick = {},
+                        modifier = Modifier.testTag("button")
+                    ) {
+                        Text("Undo", Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp))
+                    }
+                },
+                actionOnNewLine = true
+            )
+        }
+
+        val textFirstBaseLine = rule.onNodeWithText("Message").getFirstBaselinePosition()
+        val textLastBaseLine = rule.onNodeWithText("Message").getLastBaselinePosition()
+        val textBounds = rule.onNodeWithText("Message").getUnclippedBoundsInRoot()
+        val buttonBounds = rule.onNodeWithTag("button").getUnclippedBoundsInRoot()
+
+        rule.onNodeWithText("Message")
+            .assertTopPositionInRootIsEqualTo(30.dp - textFirstBaseLine)
+
+        val lastBaselineToBottom = max(18.dp, 48.dp - textLastBaseLine)
+
+        rule.onNodeWithTag("button").assertTopPositionInRootIsEqualTo(
+            lastBaselineToBottom + textBounds.top + textLastBaseLine
+        )
+
+        snackbar
+            .assertHeightIsEqualTo(2.dp + buttonBounds.top + buttonBounds.height)
+            .assertWidthIsEqualTo(8.dp + buttonBounds.left + buttonBounds.width)
+    }
+
+    @Test
+    fun defaultSnackbar_dataVersion_proxiesParameters() {
+        var clicked = false
+        val snackbarVisuals = object : SnackbarVisuals {
+            override val message: String = "Data message"
+            override val actionLabel: String = "UNDO"
+            override val withDismissAction: Boolean = false
+            override val duration: SnackbarDuration = SnackbarDuration.Short
+        }
+        val snackbarData = object : SnackbarData {
+            override val visuals: SnackbarVisuals = snackbarVisuals
+
+            override fun performAction() {
+                clicked = true
+            }
+
+            override fun dismiss() {}
+        }
+        rule.setMaterialContent(lightColorScheme()) {
+            Box {
+                Snackbar(snackbarData = snackbarData)
+            }
+        }
+
+        rule.onNodeWithText("Data message")
+            .assertExists()
+
+        assertThat(clicked).isFalse()
+
+        rule.onNodeWithText("UNDO")
+            .performClick()
+
+        assertThat(clicked).isTrue()
+    }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
index 007999b..74d8699 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
@@ -79,7 +79,7 @@
     fun noTonalElevationColorIsSetOnNonElevatedSurfaceColor() {
         var absoluteTonalElevation: Dp = 0.dp
         var surfaceColor: Color = Color.Unspecified
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             surfaceColor = MaterialTheme.colorScheme.surface
             Box(
                 Modifier
@@ -114,7 +114,7 @@
         var absoluteTonalElevation: Dp = 0.dp
         var surfaceTonalColor: Color = Color.Unspecified
         var surfaceColor: Color
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             surfaceColor = MaterialTheme.colorScheme.surface
             Box(
                 Modifier
@@ -149,7 +149,7 @@
     @Test
     fun tonalElevationColorIsNotSetOnNonSurfaceColor() {
         var absoluteTonalElevation: Dp = 0.dp
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Box(
                 Modifier
                     .size(10.dp, 10.dp)
@@ -181,7 +181,7 @@
     fun absoluteElevationCompositionLocalIsSet() {
         var outerElevation: Dp? = null
         var innerElevation: Dp? = null
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Surface(tonalElevation = 2.dp) {
                 outerElevation = LocalAbsoluteTonalElevation.current
                 Surface(tonalElevation = 4.dp) {
@@ -199,7 +199,7 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun absoluteElevationIsNotUsedForShadows() {
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Column {
                 Box(
                     Modifier
@@ -257,7 +257,7 @@
     fun contentColorSetBeforeModifier() {
         var contentColor: Color = Color.Unspecified
         val expectedColor = Color.Blue
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             CompositionLocalProvider(LocalContentColor provides Color.Red) {
                 Surface(
                     Modifier.composed {
@@ -279,7 +279,7 @@
     @Test
     fun clickableOverload_semantics() {
         val count = mutableStateOf(0)
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Surface(
                 modifier = Modifier.testTag("surface"),
                 role = Role.Checkbox,
@@ -302,7 +302,7 @@
     @Test
     fun clickableOverload_clickAction() {
         val count = mutableStateOf(0f)
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Surface(
                 modifier = Modifier.testTag("surface"),
                 onClick = { count.value += 1 }
@@ -324,7 +324,7 @@
     fun clickableOverload_enabled_disabled() {
         val count = mutableStateOf(0f)
         val enabled = mutableStateOf(true)
-        rule.setMaterialContent {
+        rule.setMaterialContent(lightColorScheme()) {
             Surface(
                 modifier = Modifier.testTag("surface"),
                 enabled = enabled.value,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index c355453..b095ebf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -1057,7 +1057,8 @@
                     // Nothing coerced, meaning we're in the middle of top app bar collapse or
                     // expand.
                     offset = coerced
-                    available
+                    // Consume only the scroll on the Y axis.
+                    available.copy(x = 0f)
                 } else {
                     Offset.Zero
                 }
@@ -1122,7 +1123,8 @@
                     // Nothing coerced, meaning we're in the middle of top app bar collapse or
                     // expand.
                     offset = coerced
-                    available
+                    // Consume only the scroll on the Y axis.
+                    available.copy(x = 0f)
                 } else {
                     Offset.Zero
                 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index 8c618ca..fed51df 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -40,9 +40,15 @@
  *
  * @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar
  *
- * @param modifier optional Modifier for the root of the [Scaffold]
+ * To show a [Snackbar], use [SnackbarHostState.showSnackbar].
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * @param modifier optional Modifier for the root of the [Scaffold].
  * @param topBar top app bar of the screen. Consider using [SmallTopAppBar].
  * @param bottomBar bottom bar of the screen. Consider using [NavigationBar].
+ * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
+ * [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost].
  * @param floatingActionButton Main action button of your screen. Consider using
  * [FloatingActionButton] for this slot.
  * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
@@ -62,19 +68,20 @@
     modifier: Modifier = Modifier,
     topBar: @Composable () -> Unit = {},
     bottomBar: @Composable () -> Unit = {},
+    snackbarHost: @Composable () -> Unit = {},
     floatingActionButton: @Composable () -> Unit = {},
     floatingActionButtonPosition: FabPosition = FabPosition.End,
     containerColor: Color = MaterialTheme.colorScheme.background,
     contentColor: Color = contentColorFor(containerColor),
     content: @Composable (PaddingValues) -> Unit
 ) {
-
     Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
         ScaffoldLayout(
             fabPosition = floatingActionButtonPosition,
             topBar = topBar,
             bottomBar = bottomBar,
             content = content,
+            snackbar = snackbarHost,
             fab = floatingActionButton
         )
     }
@@ -86,7 +93,9 @@
  * @param fabPosition [FabPosition] for the FAB (if present)
  * @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar]
  * @param content the main 'body' of the [Scaffold]
- * @param fab the [FloatingActionButton] displayed on top of the [content]
+ * @param snackbar the [Snackbar] displayed on top of the [content]
+ * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
+ * and above the [bottomBar]
  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  * [content], typically a [NavigationBar].
  */
@@ -96,6 +105,7 @@
     fabPosition: FabPosition,
     topBar: @Composable () -> Unit,
     content: @Composable (PaddingValues) -> Unit,
+    snackbar: @Composable () -> Unit,
     fab: @Composable () -> Unit,
     bottomBar: @Composable () -> Unit
 
@@ -113,6 +123,13 @@
 
             val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
 
+            val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map {
+                it.measure(looseConstraints)
+            }
+
+            val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0
+            val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0
+
             val fabPlaceables =
                 subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
                     measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
@@ -159,6 +176,12 @@
                 }
             }
 
+            val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+                snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
+            } else {
+                0
+            }
+
             val bodyContentHeight = layoutHeight - topBarHeight
 
             val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
@@ -174,6 +197,12 @@
             topBarPlaceables.forEach {
                 it.place(0, 0)
             }
+            snackbarPlaceables.forEach {
+                it.place(
+                    (layoutWidth - snackbarWidth) / 2,
+                    layoutHeight - snackbarOffsetFromBottom
+                )
+            }
             // The bottom bar is always at the bottom of the layout
             bottomBarPlaceables.forEach {
                 it.place(0, layoutHeight - bottomBarHeight)
@@ -240,4 +269,4 @@
 // FAB spacing above the bottom bar / bottom of the Scaffold
 private val FabSpacing = 16.dp
 
-private enum class ScaffoldLayoutContent { TopBar, MainContent, Fab, BottomBar }
+private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
new file mode 100644
index 0000000..fa20bb2
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.tokens.SnackbarTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.dp
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Material Design snackbar.
+ *
+ * Snackbars provide brief messages about app processes at the bottom of the screen.
+ *
+ * Snackbars inform users of a process that an app has performed or will perform. They appear
+ * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
+ * and they don’t require user input to disappear.
+ *
+ * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
+ *
+ * Snackbars with an action should not timeout or self-dismiss until the user performs another
+ * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
+ * page is not considered an action.
+ *
+ * This component provides only the visuals of the Snackbar. If you need to show a Snackbar
+ * with defaults on the screen, use [SnackbarHostState.showSnackbar]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * If you want to customize appearance of the Snackbar, you can pass your own version as a child
+ * of the [SnackbarHost] to the [Scaffold]:
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * @param modifier modifiers for the Snackbar layout
+ * @param action action / button component to add as an action to the snackbar. Consider using
+ * [ColorScheme.inversePrimary] as the color for the action, if you do not have a predefined color
+ * you wish to use instead.
+ * @param dismissAction action / button component to add as an additional close affordance action
+ * when a snackbar is non self-dismissive. Consider using [ColorScheme.inverseOnSurface] as the
+ * color for the action, if you do not have a predefined color you wish to use instead.
+ * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
+ * for action with long action text
+ * @param shape defines the Snackbar's shape (as well as its shadow when using `shadowElevation`)
+ * @param containerColor background color of the Snackbar
+ * @param contentColor the preferred color for content inside this Snackbar. Also see
+ * [LocalContentColor] which is used by [Text] and [Icon] by default.
+ * @param content content to show information about a process that an app has performed or will
+ * perform
+ */
+@Composable
+fun Snackbar(
+    modifier: Modifier = Modifier,
+    action: @Composable (() -> Unit)? = null,
+    dismissAction: @Composable (() -> Unit)? = null,
+    actionOnNewLine: Boolean = false,
+    shape: Shape = SnackbarTokens.ContainerShape,
+    containerColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.ContainerColor),
+    contentColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.SupportingTextColor),
+    content: @Composable () -> Unit
+) {
+    Surface(
+        modifier = modifier,
+        shape = shape,
+        color = containerColor,
+        contentColor = contentColor,
+        shadowElevation = SnackbarTokens.ContainerElevation
+    ) {
+        val textStyle = MaterialTheme.typography.fromToken(SnackbarTokens.SupportingTextFont)
+        CompositionLocalProvider(LocalTextStyle provides textStyle) {
+            when {
+                action == null -> OneRowSnackbar(
+                    text = content,
+                    action = null,
+                    dismissAction = dismissAction
+                )
+                actionOnNewLine -> NewLineButtonSnackbar(content, action, dismissAction)
+                else -> OneRowSnackbar(
+                    text = content,
+                    action = action,
+                    dismissAction = dismissAction
+                )
+            }
+        }
+    }
+}
+
+/**
+ * Material Design snackbar.
+ *
+ * Snackbars provide brief messages about app processes at the bottom of the screen.
+ *
+ * Snackbars inform users of a process that an app has performed or will perform. They appear
+ * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience,
+ * and they don’t require user input to disappear.
+ *
+ * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional.
+ *
+ * Snackbars with an action should not timeout or self-dismiss until the user performs another
+ * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a
+ * page is not considered an action.
+ *
+ * This version of snackbar is designed to work with [SnackbarData] provided by the
+ * [SnackbarHost], which is usually used inside of the [Scaffold].
+ *
+ * This components provides only the visuals of the Snackbar. If you need to show a Snackbar
+ * with defaults on the screen, use [SnackbarHostState.showSnackbar]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * If you want to customize appearance of the Snackbar, you can pass your own version as a child
+ * of the [SnackbarHost] to the [Scaffold]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * When a [SnackbarData.visuals] sets the Snackbar's duration as [SnackbarDuration.Indefinite], it's
+ * recommended to display an additional close affordance action.
+ * See [SnackbarVisuals.withDismissAction]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar
+ *
+ * @param snackbarData data about the current snackbar showing via [SnackbarHostState]
+ * @param modifier modifiers for the Snackbar layout
+ * @param actionOnNewLine whether or not the Snackbar's action should be put on the separate line
+ * (recommended for action with long action text)
+ * @param shape defines the Snackbar's shape (as well as its shadow when using `shadowElevation`)
+ * @param containerColor background color of the Snackbar
+ * @param contentColor the preferred color for content inside this Snackbar. Also see
+ * [LocalContentColor] which is used by [Text] and [Icon] by default.
+ * @param actionColor color of the Snackbar's action
+ */
+@Composable
+fun Snackbar(
+    snackbarData: SnackbarData,
+    modifier: Modifier = Modifier,
+    actionOnNewLine: Boolean = false,
+    shape: Shape = SnackbarTokens.ContainerShape,
+    containerColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.ContainerColor),
+    contentColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.SupportingTextColor),
+    actionColor: Color = MaterialTheme.colorScheme.fromToken(SnackbarTokens.ActionLabelTextColor)
+) {
+    val actionLabel = snackbarData.visuals.actionLabel
+    val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
+        @Composable {
+            TextButton(
+                colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
+                onClick = { snackbarData.performAction() },
+                content = { Text(actionLabel) }
+            )
+        }
+    } else {
+        null
+    }
+    val dismissActionComposable: (@Composable () -> Unit)? =
+        if (snackbarData.visuals.withDismissAction) {
+            @Composable {
+                IconButton(
+                    onClick = { snackbarData.dismiss() },
+                    content = {
+                        Icon(
+                            Icons.Filled.Close,
+                            contentDescription = null, // TODO add "Dismiss Snackbar" to Strings.
+                        )
+                    }
+                )
+            }
+        } else {
+            null
+        }
+    Snackbar(
+        modifier = modifier.padding(12.dp),
+        action = actionComposable,
+        dismissAction = dismissActionComposable,
+        actionOnNewLine = actionOnNewLine,
+        shape = shape,
+        containerColor = containerColor,
+        contentColor = contentColor,
+        content = { Text(snackbarData.visuals.message) }
+    )
+}
+
+@Composable
+private fun NewLineButtonSnackbar(
+    text: @Composable () -> Unit,
+    action: @Composable () -> Unit,
+    dismissAction: @Composable (() -> Unit)?
+) {
+    Column(
+        modifier = Modifier
+            // Fill max width, up to ContainerMaxWidth.
+            .widthIn(max = ContainerMaxWidth)
+            .fillMaxWidth()
+            .padding(
+                start = HorizontalSpacing,
+                bottom = SeparateButtonExtraY
+            )
+    ) {
+        Box(
+            Modifier.paddingFromBaseline(HeightToFirstLine, LongButtonVerticalOffset)
+                .padding(end = HorizontalSpacingButtonSide)
+        ) { text() }
+
+        Box(
+            Modifier.align(Alignment.End)
+                .padding(end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp)
+        ) {
+            Row {
+                val actionTextColor =
+                    MaterialTheme.colorScheme.fromToken(SnackbarTokens.ActionLabelTextColor)
+                val actionTextStyle =
+                    MaterialTheme.typography.fromToken(SnackbarTokens.ActionLabelTextFont)
+                CompositionLocalProvider(
+                    LocalContentColor provides actionTextColor,
+                    LocalTextStyle provides actionTextStyle,
+                    content = action
+                )
+
+                if (dismissAction != null) {
+                    val dismissActionColor =
+                        MaterialTheme.colorScheme.fromToken(SnackbarTokens.IconColor)
+                    CompositionLocalProvider(
+                        LocalContentColor provides dismissActionColor,
+                        content = dismissAction
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun OneRowSnackbar(
+    text: @Composable () -> Unit,
+    action: @Composable (() -> Unit)?,
+    dismissAction: @Composable (() -> Unit)?
+) {
+    val textTag = "text"
+    val actionTag = "action"
+    val dismissActionTag = "dismissAction"
+    Layout(
+        {
+            Box(Modifier.layoutId(textTag).padding(vertical = SnackbarVerticalPadding)) { text() }
+            if (action != null) {
+                Box(Modifier.layoutId(actionTag)) {
+                    val actionTextColor =
+                        MaterialTheme.colorScheme.fromToken(SnackbarTokens.ActionLabelTextColor)
+                    val actionTextStyle =
+                        MaterialTheme.typography.fromToken(SnackbarTokens.ActionLabelTextFont)
+                    CompositionLocalProvider(
+                        LocalContentColor provides actionTextColor,
+                        LocalTextStyle provides actionTextStyle,
+                        content = action
+                    )
+                }
+            }
+            if (dismissAction != null) {
+                Box(Modifier.layoutId(dismissActionTag)) {
+                    val dismissActionColor =
+                        MaterialTheme.colorScheme.fromToken(SnackbarTokens.IconColor)
+                    CompositionLocalProvider(
+                        LocalContentColor provides dismissActionColor,
+                        content = dismissAction
+                    )
+                }
+            }
+        },
+        modifier = Modifier.padding(
+            start = HorizontalSpacing,
+            end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp
+        )
+    ) { measurables, constraints ->
+        val containerWidth = min(constraints.maxWidth, ContainerMaxWidth.roundToPx())
+        val actionButtonPlaceable =
+            measurables.firstOrNull { it.layoutId == actionTag }?.measure(constraints)
+        val dismissButtonPlaceable =
+            measurables.firstOrNull { it.layoutId == dismissActionTag }?.measure(constraints)
+        val actionButtonWidth = actionButtonPlaceable?.width ?: 0
+        val actionButtonHeight = actionButtonPlaceable?.height ?: 0
+        val dismissButtonWidth = dismissButtonPlaceable?.width ?: 0
+        val dismissButtonHeight = dismissButtonPlaceable?.height ?: 0
+        val extraSpacingWidth = if (dismissButtonWidth == 0) TextEndExtraSpacing.roundToPx() else 0
+        val textMaxWidth =
+            (containerWidth - actionButtonWidth - dismissButtonWidth - extraSpacingWidth)
+                .coerceAtLeast(constraints.minWidth)
+        val textPlaceable = measurables.first { it.layoutId == textTag }.measure(
+            constraints.copy(minHeight = 0, maxWidth = textMaxWidth)
+        )
+
+        val firstTextBaseline = textPlaceable[FirstBaseline]
+        require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
+        val lastTextBaseline = textPlaceable[LastBaseline]
+        require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
+        val isOneLine = firstTextBaseline == lastTextBaseline
+        val dismissButtonPlaceX = containerWidth - dismissButtonWidth
+        val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth
+
+        val textPlaceY: Int
+        val containerHeight: Int
+        val actionButtonPlaceY: Int
+        if (isOneLine) {
+            val minContainerHeight = SnackbarTokens.SingleLineContainerHeight.roundToPx()
+            val contentHeight = max(actionButtonHeight, dismissButtonHeight)
+            containerHeight = max(minContainerHeight, contentHeight)
+            textPlaceY = (containerHeight - textPlaceable.height) / 2
+            actionButtonPlaceY = if (actionButtonPlaceable != null) {
+                actionButtonPlaceable[FirstBaseline].let {
+                    if (it != AlignmentLine.Unspecified) {
+                        textPlaceY + firstTextBaseline - it
+                    } else {
+                        0
+                    }
+                }
+            } else {
+                0
+            }
+        } else {
+            val baselineOffset = HeightToFirstLine.roundToPx()
+            textPlaceY = baselineOffset - firstTextBaseline
+            val minContainerHeight = SnackbarTokens.TwoLinesContainerHeight.roundToPx()
+            val contentHeight = textPlaceY + textPlaceable.height
+            containerHeight = max(minContainerHeight, contentHeight)
+            actionButtonPlaceY = if (actionButtonPlaceable != null) {
+                (containerHeight - actionButtonPlaceable.height) / 2
+            } else {
+                0
+            }
+        }
+        val dismissButtonPlaceY = if (dismissButtonPlaceable != null) {
+            (containerHeight - dismissButtonPlaceable.height) / 2
+        } else {
+            0
+        }
+
+        layout(containerWidth, containerHeight) {
+            textPlaceable.placeRelative(0, textPlaceY)
+            dismissButtonPlaceable?.placeRelative(dismissButtonPlaceX, dismissButtonPlaceY)
+            actionButtonPlaceable?.placeRelative(actionButtonPlaceX, actionButtonPlaceY)
+        }
+    }
+}
+
+private val ContainerMaxWidth = 600.dp
+private val HeightToFirstLine = 30.dp
+private val HorizontalSpacing = 16.dp
+private val HorizontalSpacingButtonSide = 8.dp
+private val SeparateButtonExtraY = 2.dp
+private val SnackbarVerticalPadding = 6.dp
+private val TextEndExtraSpacing = 8.dp
+private val LongButtonVerticalOffset = 12.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SnackbarHost.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SnackbarHost.kt
new file mode 100644
index 0000000..a382877
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SnackbarHost.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.material3
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.RecomposeScope
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.currentRecomposeScope
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.AccessibilityManager
+import androidx.compose.ui.platform.LocalAccessibilityManager
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.semantics
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlin.coroutines.resume
+
+/**
+ * State of the [SnackbarHost], controls the queue and the current [Snackbar] being shown inside
+ * the [SnackbarHost].
+ *
+ * This state usually [remember]ed and used to provide a [SnackbarHost] to a [Scaffold].
+ */
+@Stable
+class SnackbarHostState {
+
+    /**
+     * Only one [Snackbar] can be shown at a time.
+     * Since a suspending Mutex is a fair queue, this manages our message queue
+     * and we don't have to maintain one.
+     */
+    private val mutex = Mutex()
+
+    /**
+     * The current [SnackbarData] being shown by the [SnackbarHost], of `null` if none.
+     */
+    var currentSnackbarData by mutableStateOf<SnackbarData?>(null)
+        private set
+
+    /**
+     * Shows or queues to be shown a [Snackbar] at the bottom of the [Scaffold] at
+     * which this state is attached and suspends until snackbar is disappeared.
+     *
+     * [SnackbarHostState] guarantees to show at most one snackbar at a time. If this function is
+     * called while another snackbar is already visible, it will be suspended until this snack
+     * bar is shown and subsequently addressed. If the caller is cancelled, the snackbar will be
+     * removed from display and/or the queue to be displayed.
+     *
+     * All of this allows for granular control over the snackbar queue from within:
+     *
+     * @sample androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
+     *
+     * To change the Snackbar appearance, change it in 'snackbarHost' on the [Scaffold].
+     *
+     * @param message text to be shown in the Snackbar
+     * @param actionLabel optional action label to show as button in the Snackbar
+     * @param withDismissAction a boolean to show a dismiss action in the Snackbar. This is
+     * recommended to be set to true for better accessibility when a Snackbar is set with a
+     * [SnackbarDuration.Indefinite]
+     * @param duration duration to control how long snackbar will be shown in [SnackbarHost], either
+     * [SnackbarDuration.Short], [SnackbarDuration.Long] or [SnackbarDuration.Indefinite].
+     *
+     * @return [SnackbarResult.ActionPerformed] if option action has been clicked or
+     * [SnackbarResult.Dismissed] if snackbar has been dismissed via timeout or by the user
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    suspend fun showSnackbar(
+        message: String,
+        actionLabel: String? = null,
+        withDismissAction: Boolean = false,
+        duration: SnackbarDuration = SnackbarDuration.Short
+    ): SnackbarResult =
+        showSnackbar(SnackbarVisualsImpl(message, actionLabel, withDismissAction, duration))
+
+    /**
+     * Shows or queues to be shown a [Snackbar] at the bottom of the [Scaffold] at
+     * which this state is attached and suspends until snackbar is disappeared.
+     *
+     * [SnackbarHostState] guarantees to show at most one snackbar at a time. If this function is
+     * called while another snackbar is already visible, it will be suspended until this snack
+     * bar is shown and subsequently addressed. If the caller is cancelled, the snackbar will be
+     * removed from display and/or the queue to be displayed.
+     *
+     * All of this allows for granular control over the snackbar queue from within:
+     *
+     * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+     *
+     * @param visuals [SnackbarVisuals] that are used to create a Snackbar
+     *
+     * @return [SnackbarResult.ActionPerformed] if option action has been clicked or
+     * [SnackbarResult.Dismissed] if snackbar has been dismissed via timeout or by the user
+     */
+    @ExperimentalMaterial3Api
+    suspend fun showSnackbar(visuals: SnackbarVisuals): SnackbarResult = mutex.withLock {
+        try {
+            return suspendCancellableCoroutine { continuation ->
+                currentSnackbarData = SnackbarDataImpl(visuals, continuation)
+            }
+        } finally {
+            currentSnackbarData = null
+        }
+    }
+
+    private class SnackbarVisualsImpl(
+        override val message: String,
+        override val actionLabel: String?,
+        override val withDismissAction: Boolean,
+        override val duration: SnackbarDuration
+    ) : SnackbarVisuals {
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other == null || this::class != other::class) return false
+
+            other as SnackbarVisualsImpl
+
+            if (message != other.message) return false
+            if (actionLabel != other.actionLabel) return false
+            if (withDismissAction != other.withDismissAction) return false
+            if (duration != other.duration) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = message.hashCode()
+            result = 31 * result + actionLabel.hashCode()
+            result = 31 * result + withDismissAction.hashCode()
+            result = 31 * result + duration.hashCode()
+            return result
+        }
+    }
+
+    private class SnackbarDataImpl(
+        override val visuals: SnackbarVisuals,
+        private val continuation: CancellableContinuation<SnackbarResult>
+    ) : SnackbarData {
+
+        override fun performAction() {
+            if (continuation.isActive) continuation.resume(SnackbarResult.ActionPerformed)
+        }
+
+        override fun dismiss() {
+            if (continuation.isActive) continuation.resume(SnackbarResult.Dismissed)
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other == null || this::class != other::class) return false
+
+            other as SnackbarDataImpl
+
+            if (visuals != other.visuals) return false
+            if (continuation != other.continuation) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = visuals.hashCode()
+            result = 31 * result + continuation.hashCode()
+            return result
+        }
+    }
+}
+
+/**
+ * Host for [Snackbar]s to be used in [Scaffold] to properly show, hide and dismiss items based
+ * on material specification and the [hostState].
+ *
+ * This component with default parameters comes build-in with [Scaffold], if you need to show a
+ * default [Snackbar], use [SnackbarHostState.showSnackbar].
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
+ *
+ * If you want to customize appearance of the [Snackbar], you can pass your own version as a child
+ * of the [SnackbarHost] to the [Scaffold]:
+ *
+ * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
+ *
+ * @param hostState state of this component to read and show [Snackbar]s accordingly
+ * @param modifier optional modifier for this component
+ * @param snackbar the instance of the [Snackbar] to be shown at the appropriate time with
+ * appearance based on the [SnackbarData] provided as a param
+ */
+@Composable
+fun SnackbarHost(
+    hostState: SnackbarHostState,
+    modifier: Modifier = Modifier,
+    snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) }
+) {
+    val currentSnackbarData = hostState.currentSnackbarData
+    val accessibilityManager = LocalAccessibilityManager.current
+    LaunchedEffect(currentSnackbarData) {
+        if (currentSnackbarData != null) {
+            val duration = currentSnackbarData.visuals.duration.toMillis(
+                currentSnackbarData.visuals.actionLabel != null,
+                accessibilityManager
+            )
+            delay(duration)
+            currentSnackbarData.dismiss()
+        }
+    }
+    FadeInFadeOutWithScale(
+        current = hostState.currentSnackbarData,
+        modifier = modifier,
+        content = snackbar
+    )
+}
+
+/**
+ * Interface to represent the visuals of one particular [Snackbar] as a piece of the [SnackbarData].
+ *
+ * @property message text to be shown in the Snackbar
+ * @property actionLabel optional action label to show as button in the Snackbar
+ * @property withDismissAction a boolean to show a dismiss action in the Snackbar. This is
+ * recommended to be set to true better accessibility when a Snackbar is set with a
+ * [SnackbarDuration.Indefinite]
+ * @property duration duration of the Snackbar
+ */
+@Stable
+interface SnackbarVisuals {
+    val message: String
+    val actionLabel: String?
+    val withDismissAction: Boolean
+    val duration: SnackbarDuration
+}
+
+/**
+ * Interface to represent the data of one particular [Snackbar] as a piece of the
+ * [SnackbarHostState].
+ *
+ * @property visuals Holds the visual representation for a particular [Snackbar].
+ */
+@Stable
+interface SnackbarData {
+    val visuals: SnackbarVisuals
+
+    /**
+     * Function to be called when Snackbar action has been performed to notify the listeners.
+     */
+    fun performAction()
+
+    /**
+     * Function to be called when Snackbar is dismissed either by timeout or by the user.
+     */
+    fun dismiss()
+}
+
+/**
+ * Possible results of the [SnackbarHostState.showSnackbar] call
+ */
+enum class SnackbarResult {
+    /**
+     * [Snackbar] that is shown has been dismissed either by timeout of by user
+     */
+    Dismissed,
+
+    /**
+     * Action on the [Snackbar] has been clicked before the time out passed
+     */
+    ActionPerformed,
+}
+
+/**
+ * Possible durations of the [Snackbar] in [SnackbarHost]
+ */
+enum class SnackbarDuration {
+    /**
+     * Show the Snackbar for a short period of time
+     */
+    Short,
+
+    /**
+     * Show the Snackbar for a long period of time
+     */
+    Long,
+
+    /**
+     * Show the Snackbar indefinitely until explicitly dismissed or action is clicked
+     */
+    Indefinite
+}
+
+// TODO: magic numbers adjustment
+internal fun SnackbarDuration.toMillis(
+    hasAction: Boolean,
+    accessibilityManager: AccessibilityManager?
+): Long {
+    val original = when (this) {
+        SnackbarDuration.Indefinite -> Long.MAX_VALUE
+        SnackbarDuration.Long -> 10000L
+        SnackbarDuration.Short -> 4000L
+    }
+    if (accessibilityManager == null) {
+        return original
+    }
+    return accessibilityManager.calculateRecommendedTimeoutMillis(
+        original,
+        containsIcons = true,
+        containsText = true,
+        containsControls = hasAction
+    )
+}
+
+// TODO: to be replaced with the public customizable implementation
+// it's basically tweaked nullable version of Crossfade
+@Composable
+private fun FadeInFadeOutWithScale(
+    current: SnackbarData?,
+    modifier: Modifier = Modifier,
+    content: @Composable (SnackbarData) -> Unit
+) {
+    val state = remember { FadeInFadeOutState<SnackbarData?>() }
+    if (current != state.current) {
+        state.current = current
+        val keys = state.items.map { it.key }.toMutableList()
+        if (!keys.contains(current)) {
+            keys.add(current)
+        }
+        state.items.clear()
+        keys.filterNotNull().mapTo(state.items) { key ->
+            FadeInFadeOutAnimationItem(key) { children ->
+                val isVisible = key == current
+                val duration = if (isVisible) SnackbarFadeInMillis else SnackbarFadeOutMillis
+                val delay = SnackbarFadeOutMillis + SnackbarInBetweenDelayMillis
+                val animationDelay = if (isVisible && keys.filterNotNull().size != 1) delay else 0
+                val opacity = animatedOpacity(
+                    animation = tween(
+                        easing = LinearEasing,
+                        delayMillis = animationDelay,
+                        durationMillis = duration
+                    ),
+                    visible = isVisible,
+                    onAnimationFinish = {
+                        if (key != state.current) {
+                            // leave only the current in the list
+                            state.items.removeAll { it.key == key }
+                            state.scope?.invalidate()
+                        }
+                    }
+                )
+                val scale = animatedScale(
+                    animation = tween(
+                        easing = FastOutSlowInEasing,
+                        delayMillis = animationDelay,
+                        durationMillis = duration
+                    ),
+                    visible = isVisible
+                )
+                Box(
+                    Modifier
+                        .graphicsLayer(
+                            scaleX = scale.value,
+                            scaleY = scale.value,
+                            alpha = opacity.value
+                        )
+                        .semantics {
+                            liveRegion = LiveRegionMode.Polite
+                            dismiss { key.dismiss(); true }
+                        }
+                ) {
+                    children()
+                }
+            }
+        }
+    }
+    Box(modifier) {
+        state.scope = currentRecomposeScope
+        state.items.forEach { (item, opacity) ->
+            key(item) {
+                opacity {
+                    content(item!!)
+                }
+            }
+        }
+    }
+}
+
+private class FadeInFadeOutState<T> {
+    // we use Any here as something which will not be equals to the real initial value
+    var current: Any? = Any()
+    var items = mutableListOf<FadeInFadeOutAnimationItem<T>>()
+    var scope: RecomposeScope? = null
+}
+
+private data class FadeInFadeOutAnimationItem<T>(
+    val key: T,
+    val transition: FadeInFadeOutTransition
+)
+
+private typealias FadeInFadeOutTransition = @Composable (content: @Composable () -> Unit) -> Unit
+
+@Composable
+private fun animatedOpacity(
+    animation: AnimationSpec<Float>,
+    visible: Boolean,
+    onAnimationFinish: () -> Unit = {}
+): State<Float> {
+    val alpha = remember { Animatable(if (!visible) 1f else 0f) }
+    LaunchedEffect(visible) {
+        alpha.animateTo(
+            if (visible) 1f else 0f,
+            animationSpec = animation
+        )
+        onAnimationFinish()
+    }
+    return alpha.asState()
+}
+
+@Composable
+private fun animatedScale(animation: AnimationSpec<Float>, visible: Boolean): State<Float> {
+    val scale = remember { Animatable(if (!visible) 1f else 0.8f) }
+    LaunchedEffect(visible) {
+        scale.animateTo(
+            if (visible) 1f else 0.8f,
+            animationSpec = animation
+        )
+    }
+    return scale.asState()
+}
+
+private const val SnackbarFadeInMillis = 150
+private const val SnackbarFadeOutMillis = 75
+private const val SnackbarInBetweenDelayMillis = 0
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt
new file mode 100644
index 0000000..f536558
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ShapeTokens.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.unit.dp
+
+// TODO(b/209589029): Remove once the shape subsystem is in place.
+internal object ShapeTokens {
+    val Large = RoundedCornerShape(8.0.dp)
+    val Medium = RoundedCornerShape(8.0.dp)
+    val Small = RoundedCornerShape(4.0.dp)
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SnackbarTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SnackbarTokens.kt
new file mode 100644
index 0000000..16b4b0c
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SnackbarTokens.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SnackbarTokens {
+    val ActionFocusLabelTextColor = ColorSchemeKey.InversePrimary
+    val ActionFocusStateLayerColor = ColorSchemeKey.InversePrimary
+    val ActionHoverLabelTextColor = ColorSchemeKey.InversePrimary
+    val ActionHoverStateLayerColor = ColorSchemeKey.InversePrimary
+    val ActionLabelTextColor = ColorSchemeKey.InversePrimary
+    val ActionLabelTextFont = TypographyKey.LabelLarge
+    val ActionPressedLabelTextColor = ColorSchemeKey.InversePrimary
+    val ActionPressedStateLayerColor = ColorSchemeKey.InversePrimary
+    val ContainerColor = ColorSchemeKey.InverseSurface
+    val ContainerElevation = Elevation.Level3
+    val ContainerShape = ShapeTokens.Small
+    val IconColor = ColorSchemeKey.InverseOnSurface
+    val IconSize = 24.0.dp
+    val SupportingTextColor = ColorSchemeKey.InverseOnSurface
+    val SupportingTextFont = TypographyKey.BodyMedium
+    val SingleLineContainerHeight = 48.0.dp
+    val TwoLinesContainerHeight = 68.0.dp
+}
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 27fddc4..bdbbd99 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -783,6 +783,9 @@
     field public static final androidx.compose.runtime.snapshots.SnapshotApplyResult.Success INSTANCE;
   }
 
+  public final class SnapshotDoubleIndexHeapKt {
+  }
+
   public final class SnapshotIdSetKt {
   }
 
@@ -896,10 +899,12 @@
 
   public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
     method public Iterable<java.lang.Object> getData();
+    method public default Object? getIdentity();
     method public Object getKey();
     method public Object? getNode();
     method public String? getSourceInfo();
     property public abstract Iterable<java.lang.Object> data;
+    property public default Object? identity;
     property public abstract Object key;
     property public abstract Object? node;
     property public abstract String? sourceInfo;
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 2f9a57e..f4eda00 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -818,6 +818,9 @@
     field public static final androidx.compose.runtime.snapshots.SnapshotApplyResult.Success INSTANCE;
   }
 
+  public final class SnapshotDoubleIndexHeapKt {
+  }
+
   public final class SnapshotIdSetKt {
   }
 
@@ -931,10 +934,12 @@
 
   public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
     method public Iterable<java.lang.Object> getData();
+    method public default Object? getIdentity();
     method public Object getKey();
     method public Object? getNode();
     method public String? getSourceInfo();
     property public abstract Iterable<java.lang.Object> data;
+    property public default Object? identity;
     property public abstract Object key;
     property public abstract Object? node;
     property public abstract String? sourceInfo;
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 5b450a1..e224229 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -815,6 +815,9 @@
     field public static final androidx.compose.runtime.snapshots.SnapshotApplyResult.Success INSTANCE;
   }
 
+  public final class SnapshotDoubleIndexHeapKt {
+  }
+
   public final class SnapshotIdSetKt {
   }
 
@@ -934,10 +937,12 @@
 
   public interface CompositionGroup extends androidx.compose.runtime.tooling.CompositionData {
     method public Iterable<java.lang.Object> getData();
+    method public default Object? getIdentity();
     method public Object getKey();
     method public Object? getNode();
     method public String? getSourceInfo();
     property public abstract Iterable<java.lang.Object> data;
+    property public default Object? identity;
     property public abstract Object key;
     property public abstract Object? node;
     property public abstract String? sourceInfo;
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index adcb14c..3325134 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -21,9 +21,9 @@
 import androidx.compose.runtime.snapshots.fastForEach
 import androidx.compose.runtime.snapshots.fastMap
 import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
 import kotlin.math.max
 import kotlin.math.min
-import androidx.compose.runtime.tooling.CompositionGroup
 
 // Nomenclature -
 // Address      - an absolute offset into the array ignoring its gap. See Index below.
@@ -2580,6 +2580,12 @@
 
             override val data: Iterable<Any?> get() = DataIterator(table, group)
 
+            override val identity: Any
+                get() {
+                    validateRead()
+                    return table.read { it.anchor(group) }
+                }
+
             override val compositionGroups: Iterable<CompositionGroup> get() = this
 
             override fun iterator(): Iterator<CompositionGroup> {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index fe17ea1..22cd1fc 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -75,6 +75,7 @@
      */
     open fun dispose() {
         disposed = true
+        releasePinnedSnapshot()
     }
 
     /**
@@ -133,6 +134,13 @@
     internal var disposed = false
 
     /*
+     *
+     */
+    @Suppress("LeakingThis")
+    private var pinningTrackingHandle =
+        if (id != INVALID_SNAPSHOT) trackPinning(id, invalid) else -1
+
+    /*
      * The read observer for the snapshot if there is one.
      */
     internal abstract val readObserver: ((Any) -> Unit)?
@@ -181,6 +189,7 @@
     internal open fun close() {
         sync {
             openSnapshots = openSnapshots.clear(id)
+            releasePinnedSnapshot()
         }
     }
 
@@ -188,6 +197,16 @@
         require(!disposed) { "Cannot use a disposed snapshot" }
     }
 
+    internal fun releasePinnedSnapshot() {
+        if (pinningTrackingHandle >= 0) {
+            releasePinning(pinningTrackingHandle)
+            pinningTrackingHandle = -1
+        }
+    }
+
+    internal fun takeoverPinnedSnapshot(): Int =
+        pinningTrackingHandle.also { pinningTrackingHandle = -1 }
+
     companion object {
         /**
          * Return the thread's active snapshot. If no thread snapshot is active then the current
@@ -479,6 +498,28 @@
 }
 
 /**
+ * Pin the snapshot and invalid set.
+ *
+ * @return returns a handle that should be passed to [releasePinning] when the snapshot closes or
+ * is disposed.
+ */
+internal fun trackPinning(id: Int, invalid: SnapshotIdSet): Int {
+    val pinned = invalid.lowest(id)
+    return sync {
+        pinningTable.add(pinned)
+    }
+}
+
+/**
+ * Release the [handle] returned by [trackPinning]
+ */
+internal fun releasePinning(handle: Int) {
+    sync {
+        pinningTable.remove(handle)
+    }
+}
+
+/**
  * A snapshot of the values return by mutable states and other state objects. All state object
  * will have the same value in the snapshot as they had when the snapshot was created unless they
  * are explicitly changed in the snapshot.
@@ -681,6 +722,8 @@
         sync {
             // Remove itself and previous ids from the open set.
             openSnapshots = openSnapshots.clear(id).andNot(previousIds)
+            releasePinnedSnapshot()
+            releasePreviouslyPinnedSnapshots()
         }
     }
 
@@ -825,6 +868,25 @@
         }
     }
 
+    internal fun recordPreviousPinnedSnapshot(id: Int) {
+        if (id >= 0)
+            previousPinnedSnapshots = previousPinnedSnapshots + id
+    }
+
+    internal fun recordPreviousPinnedSnapshots(handles: IntArray) {
+        // Avoid unnecessary copies implied by the `+` below.
+        if (handles.isEmpty()) return
+        val pinned = previousPinnedSnapshots
+        if (pinned.isEmpty()) previousPinnedSnapshots = handles
+        else previousPinnedSnapshots = pinned + handles
+    }
+
+    internal fun releasePreviouslyPinnedSnapshots() {
+        for (index in previousPinnedSnapshots.indices) {
+            releasePinning(previousPinnedSnapshots[index])
+        }
+    }
+
     internal fun recordPreviousList(snapshots: SnapshotIdSet) {
         sync {
             previousIds = previousIds.or(snapshots)
@@ -844,6 +906,11 @@
     internal var previousIds: SnapshotIdSet = SnapshotIdSet.EMPTY
 
     /**
+     * A list of the pinned snapshots handles that must be released by this snapshot
+     */
+    internal var previousPinnedSnapshots: IntArray = IntArray(0)
+
+    /**
      * The number of pending nested snapshots of this snapshot. To simplify the code, this
      * snapshot it, itself, counted as its own nested snapshot.
      */
@@ -1165,10 +1232,7 @@
         error("Cannot apply the global snapshot directly. Call Snapshot.advanceGlobalSnapshot")
 
     override fun dispose() {
-        // Disposing the global snapshot is a no-op.
-
-        // The dispose behavior is performed by advancing the global snapshot. This method is
-        // squelched so  calling it from `currentSnapshot` doesn't cause incorrect behavior
+        releasePinnedSnapshot()
     }
 }
 
@@ -1236,7 +1300,9 @@
 
             // Ensure the ids associated with this snapshot are also applied by the parent.
             parent.recordPrevious(id)
+            parent.recordPreviousPinnedSnapshot(takeoverPinnedSnapshot())
             parent.recordPreviousList(previousIds)
+            parent.recordPreviousPinnedSnapshots(previousPinnedSnapshots)
         }
 
         applied = true
@@ -1382,6 +1448,13 @@
 /** The first snapshot created must be at least on more than the INVALID_SNAPSHOT */
 private var nextSnapshotId = INVALID_SNAPSHOT + 1
 
+/**
+ * A tracking table for pinned snapshots. A pinned snapshot is the lowest snapshot id that the
+ * snapshot is ignoring by considering them invalid. This is used to calculate when a snapshot
+ * record can be reused.
+ */
+private val pinningTable = SnapshotDoubleIndexHeap()
+
 /** A list of apply observers */
 private val applyObservers = mutableListOf<(Set<Any>, Snapshot) -> Unit>()
 
@@ -1425,6 +1498,7 @@
                 invalid = openSnapshots
             )
         )
+        previousGlobalSnapshot.dispose()
         openSnapshots = openSnapshots.set(globalId)
     }
 
@@ -1534,10 +1608,11 @@
  * record created in an abandoned snapshot. It is also true if the record is valid in the
  * previous snapshot and is obscured by another record also valid in the previous state record.
  */
-private fun used(state: StateObject, id: Int, invalid: SnapshotIdSet): StateRecord? {
+private fun used(state: StateObject): StateRecord? {
     var current: StateRecord? = state.firstStateRecord
     var validRecord: StateRecord? = null
-    val lowestOpen = invalid.lowest(id)
+    val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1
+    val invalid = SnapshotIdSet.EMPTY
     while (current != null) {
         val currentId = current.snapshotId
         if (currentId == INVALID_SNAPSHOT) {
@@ -1545,7 +1620,7 @@
             // immediately.
             return current
         }
-        if (valid(current, lowestOpen, invalid)) {
+        if (valid(current, reuseLimit, invalid)) {
             if (validRecord == null) {
                 validRecord = current
             } else {
@@ -1593,7 +1668,7 @@
 
     if (candidate.snapshotId == id) return candidate
 
-    val newData = newOverwritableRecord(state, snapshot)
+    val newData = newOverwritableRecord(state)
     newData.snapshotId = id
 
     snapshot.recordModified(state)
@@ -1614,13 +1689,13 @@
     // cache the result of readable() as the mutating thread calls to writable() can change the
     // result of readable().
     @Suppress("UNCHECKED_CAST")
-    val newData = newOverwritableRecord(state, snapshot)
+    val newData = newOverwritableRecord(state)
     newData.assign(this)
     newData.snapshotId = snapshot.id
     return newData
 }
 
-internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject, snapshot: Snapshot): T {
+internal fun <T : StateRecord> T.newOverwritableRecord(state: StateObject): T {
     // Calling used() on a state object might return the same record for each thread calling
     // used() therefore selecting the record to reuse should be guarded.
 
@@ -1633,7 +1708,7 @@
     // cache the result of readable() as the mutating thread calls to writable() can change the
     // result of readable().
     @Suppress("UNCHECKED_CAST")
-    return (used(state, snapshot.id, openSnapshots) as T?)?.apply {
+    return (used(state) as T?)?.apply {
         snapshotId = Int.MAX_VALUE
     } ?: create().apply {
         snapshotId = Int.MAX_VALUE
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap.kt
new file mode 100644
index 0000000..12241e4
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeap.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.runtime.snapshots
+
+import androidx.compose.runtime.TestOnly
+
+/**
+ * This class maintains returns the lowest number of all the number it is given and can return that
+ * number in O(1) time. Adding a number is at worst O(log N). Adding a number returns a handled that
+ * can be later used to remove the number also with at worst O(log N).
+ *
+ * The data structure used is a heap, the first stage of a heap sort. As values are added and
+ * removed the heap invariants are reestablished for the new value by either shifting values up
+ * or down in the heap.
+ *
+ * This class is used to track the lowest pinning snapshot id. A pinning snapshot id is either the
+ * lowest snapshot in its invalid list or its own id if its invalid list is empty.
+ *
+ * If any snapshot object has two records below the lowest pinned snapshot then the lowest snapshot
+ * id can be reused as it will never be selected as the current record of the object because the
+ * record with the higher id will always be selected instead.
+ */
+internal class SnapshotDoubleIndexHeap {
+    var size = 0
+        private set
+    // An array of values which are the snapshot ids
+    private var values = IntArray(INITIAL_CAPACITY)
+
+    // An array of where the value's handle is in the handles array.
+    private var index = IntArray(INITIAL_CAPACITY)
+
+    // An array of handles which tracks where the value is the values array. Free handles are stored
+    // as a single linked list using the array value as the link to the next free handle location.
+    // It is initialized with 1, 2, 3, ... which produces a linked list of all handles free starting
+    // at 0.
+    private var handles = IntArray(INITIAL_CAPACITY) { it + 1 }
+
+    // The first free handle.
+    private var firstFreeHandle = 0
+
+    fun lowestOrDefault(default: Int = 0) = if (size > 0) values[0] else default
+
+    /**
+     * Add a value to the heap by adding it to the end of the heap and then shifting it up until
+     * it is either at the root or its parent is less or equal to it.
+     */
+    fun add(value: Int): Int {
+        ensure(size + 1)
+        val i = size++
+        val handle = allocateHandle()
+        values[i] = value
+        index[i] = handle
+        handles[handle] = i
+        shiftUp(i)
+        return handle
+    }
+
+    /**
+     * Remove a value by using the index to locate where it is in the heap then replacing its
+     * location with the last member of the heap and shifting it up or down depending to restore
+     * the heap invariants.
+     */
+    fun remove(handle: Int) {
+        val i = handles[handle]
+        swap(i, size - 1)
+        size--
+        shiftUp(i)
+        shiftDown(i)
+        freeHandle(handle)
+    }
+
+    /**
+     * Validate that the heap invariants hold.
+     */
+    @TestOnly
+    fun validate() {
+        for (index in 1 until size) {
+            val parent = ((index + 1) shr 1) - 1
+            if (values[parent] > values[index]) error("Index $index is out of place")
+        }
+    }
+
+    /**
+     * Validate that the handle refers to the expected value.
+     */
+    @TestOnly
+    fun validateHandle(handle: Int, value: Int) {
+        val i = handles[handle]
+        if (index[i] != handle) error("Index for handle $handle is corrupted")
+        if (values[i] != value)
+            error("Value for handle $handle was ${values[i]} but was supposed to be $value")
+    }
+
+    /**
+     * Shift a value at [index] until its parent is less than it is or it is at index 0.
+     */
+    private fun shiftUp(index: Int) {
+        val values = values
+        val value = values[index]
+        var current = index
+        while (current > 0) {
+            val parent = ((current + 1) shr 1) - 1
+            if (values[parent] > value) {
+                swap(parent, current)
+                current = parent
+                continue
+            }
+            break
+        }
+    }
+
+    /**
+     * Shift a value at [index] down by comparing it to the least of its children and swapping with
+     * it if the child is less than it is, continuing until the index has no children.
+     */
+    private fun shiftDown(index: Int) {
+        val values = values
+        val half = size shr 1
+        var current = index
+        while (current < half) {
+            val right = (current + 1) shl 1
+            val left = right - 1
+            if (right < size && values[right] < values[left]) {
+                if (values[right] < values[current]) {
+                    swap(right, current)
+                    current = right
+                } else
+                    return
+            } else if (values[left] < values[current]) {
+                swap(left, current)
+                current = left
+            } else
+                return
+        }
+    }
+
+    /**
+     * Swap the values at index [a] and [b]. This is used to restore the heap invariants in
+     * [shiftUp] and [shiftDown]. It also ensures that the [index] and [handles] are updated to
+     * account for the swap.
+     */
+    private fun swap(a: Int, b: Int) {
+        val values = values
+        val index = index
+        val handles = handles
+        var t = values[a]
+        values[a] = values[b]
+        values[b] = t
+        t = index[a]
+        index[a] = index[b]
+        index[b] = t
+        handles[index[a]] = a
+        handles[index[b]] = b
+    }
+
+    /**
+     * Ensure that the heap can contain at least [atLeast] elements.
+     */
+    private fun ensure(atLeast: Int) {
+        val capacity = values.size
+        if (atLeast <= capacity) return
+        val newCapacity = capacity * 2
+        val newValues = IntArray(newCapacity)
+        val newIndex = IntArray(newCapacity)
+        values.copyInto(newValues)
+        index.copyInto(newIndex)
+        values = newValues
+        index = newIndex
+    }
+
+    /**
+     * Allocate a free handle, growing the list of handles if necessary.
+     */
+    private fun allocateHandle(): Int {
+        val capacity = handles.size
+        if (firstFreeHandle >= capacity) {
+            val newHandles = IntArray(capacity * 2) { it + 1 }
+            handles.copyInto(newHandles)
+            handles = newHandles
+        }
+        val handle = firstFreeHandle
+        firstFreeHandle = handles[firstFreeHandle]
+        return handle
+    }
+
+    /**
+     * Free a handle by adding it to the free list of handles which is a linked list of handles
+     * stored in the handles array as a linked list of indexes.
+     */
+    private fun freeHandle(handle: Int) {
+        handles[handle] = firstFreeHandle
+        firstFreeHandle = handle
+    }
+}
+
+private const val INITIAL_CAPACITY = 16
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
index 19a2458..0d64142 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
@@ -237,6 +237,33 @@
         }
     }
 
+    fun and(bits: SnapshotIdSet): SnapshotIdSet {
+        if (bits == EMPTY) return EMPTY
+        if (this == EMPTY) return EMPTY
+        return if (bits.lowerBound == this.lowerBound && bits.belowBound === this.belowBound) {
+            val newUpper = this.upperSet and bits.upperSet
+            val newLower = this.lowerSet and bits.lowerSet
+            if (newUpper == 0L && newLower == 0L && this.belowBound == null)
+                EMPTY
+            else
+                SnapshotIdSet(
+                    this.upperSet and bits.upperSet,
+                    this.lowerSet and bits.lowerSet,
+                    this.lowerBound,
+                    this.belowBound
+                )
+        } else {
+            if (this.belowBound == null)
+                this.fold(EMPTY) { previous, index ->
+                    if (bits.get(index)) previous.set(index) else previous
+                }
+            else
+                bits.fold(EMPTY) { previous, index ->
+                    if (this.get(index)) previous.set(index) else previous
+                }
+        }
+    }
+
     /**
      * Produce a set that if the value is set in this set or [bits] (`a | b`)
      */
@@ -283,6 +310,28 @@
         }
     }.iterator()
 
+    inline fun fastForEach(block: (Int) -> Unit) {
+        val belowBound = belowBound
+        if (belowBound != null)
+            for (element in belowBound) {
+                block(element)
+            }
+        if (lowerSet != 0L) {
+            for (index in 0 until Long.SIZE_BITS) {
+                if (lowerSet and (1L shl index) != 0L) {
+                    block(index + lowerBound)
+                }
+            }
+        }
+        if (upperSet != 0L) {
+            for (index in 0 until Long.SIZE_BITS) {
+                if (upperSet and (1L shl index) != 0L) {
+                    block(index + Long.SIZE_BITS + lowerBound)
+                }
+            }
+        }
+    }
+
     fun lowest(default: Int): Int {
         val belowBound = belowBound
         if (belowBound != null) return belowBound[0]
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
index 630f878..cc1edd6 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/CompositionData.kt
@@ -74,4 +74,10 @@
      * [remember] and the last value returned by [remember], etc.
      */
     val data: Iterable<Any?>
+
+    /**
+     * A value that identifies a Group independently of movement caused by recompositions.
+     */
+    val identity: Any?
+      get() = null
 }
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index bb0dc99..2df65f4 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -3229,6 +3229,41 @@
         threadException?.let { throw it }
     }
 
+    @Test
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun avoidRaceConditionWhenApplyingSnapshotsInAThread() = compositionTest {
+        val count = mutableStateOf(0)
+        var threadException: Exception? = null
+
+        compose {
+            Text("Some text")
+            Text("Count ${count.value}")
+        }
+
+        val thread = thread {
+            try {
+                while (!Thread.interrupted()) {
+                    Snapshot.withMutableSnapshot {
+                        count.value++
+                    }
+                }
+            } catch (e: Exception) {
+                threadException = e
+            }
+        }
+
+        repeat(200) {
+            advance(ignorePendingWork = true)
+            delay(1)
+        }
+
+        thread.interrupt()
+        @Suppress("BlockingMethodInNonBlockingContext")
+        thread.join()
+        delay(10)
+        threadException?.let { throw it }
+    }
+
     @Test // b/197064250 and others
     fun canInvalidateDuringApplyChanges() = compositionTest {
         var value by mutableStateOf(0)
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeapTests.kt
new file mode 100644
index 0000000..03590c9
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotDoubleIndexHeapTests.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.compose.runtime.snapshots
+
+import kotlin.random.Random
+import kotlin.random.nextInt
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class SnapshotDoubleIndexHeapTests {
+
+    @Test
+    fun canCreateADoubleIndexHeap() {
+        val heap = SnapshotDoubleIndexHeap()
+        assertNotNull(heap)
+    }
+
+    @Test
+    fun canAddAndRemoveNumbersInSequence() {
+        val heap = SnapshotDoubleIndexHeap()
+        val handles = IntArray(100)
+        repeat(100) {
+            handles[it] = heap.add(it)
+        }
+        repeat(100) {
+            assertEquals(it, heap.lowestOrDefault(-1))
+            heap.remove(handles[it])
+        }
+        assertEquals(0, heap.size)
+    }
+
+    @Test
+    fun canInsertAndRemoveRandomNumbersWithDuplicate() {
+        val heap = SnapshotDoubleIndexHeap()
+        val random = Random(1377)
+        val toAdd = IntArray(5000) { random.nextInt(0 until 300) }.toMutableList()
+        val toRemove = mutableListOf<Pair<Int, Int>>()
+
+        while (toAdd.size > 0 || toRemove.size > 0) {
+            val shouldAdd = random.nextInt(toAdd.size + toRemove.size) < toAdd.size
+            if (shouldAdd) {
+                val indexToAdd = random.nextInt(toAdd.size)
+                val value = toAdd[indexToAdd]
+                val handle = heap.add(value)
+                toRemove.add(value to handle)
+                toAdd.removeAt(indexToAdd)
+            } else {
+                val indexToRemove = random.nextInt(toRemove.size)
+                val (value, handle) = toRemove[indexToRemove]
+                assertTrue(heap.lowestOrDefault(-1) <= value)
+                heap.remove(handle)
+                toRemove.removeAt(indexToRemove)
+            }
+
+            heap.validate()
+            for ((value, handle) in toRemove) {
+                heap.validateHandle(handle, value)
+            }
+            val lowestAdded = toRemove.fold(400) { lowest, (value, _) ->
+                if (value < lowest) value else lowest
+            }
+            assertEquals(lowestAdded, heap.lowestOrDefault(400))
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index f01ebb3..da8ca1a 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -28,6 +28,8 @@
 import androidx.compose.runtime.referentialEqualityPolicy
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot.Companion.openSnapshotCount
+import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot
+import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot
 import androidx.compose.runtime.structuralEqualityPolicy
 import kotlin.test.AfterTest
 import kotlin.test.BeforeTest
@@ -798,6 +800,21 @@
         }
     }
 
+    @Test
+    fun testRecordsAreReusedCorrectly() {
+        val value = mutableStateOf(0)
+        Snapshot.withMutableSnapshot { value.value++ }
+        val mutable1 = takeMutableSnapshot()
+        val readable1 = takeSnapshot()
+        mutable1.enter { value.value++ }
+        mutable1.apply().check()
+        Snapshot.withMutableSnapshot { value.value++ }
+        val v = readable1.enter { value.value }
+        assertEquals(v, 1)
+        readable1.dispose()
+        mutable1.dispose()
+    }
+
     private var count = 0
 
     @BeforeTest
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
index 46b3aab..e48c49a 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
@@ -800,23 +800,34 @@
         val androidComposeView = findAndroidComposeView()
         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
         val builder = LayoutInspectorTree()
-        builder.hideSystemNodes = false
-        val first = builder.convert(androidComposeView)
-            .flatMap { flatten(it) }
-            .first { it.name == "First" }
+        val tree1 = builder.convert(androidComposeView)
+        val first = tree1.flatMap { flatten(it) }.single { it.name == "First" }
         val hash = packageNameHash(this.javaClass.name.substringBeforeLast('.'))
         assertThat(first.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
         assertThat(first.packageHash).isEqualTo(hash)
         assertThat(first.parameters.map { it.name }).contains("p1")
 
+        val cross1 = tree1.flatMap { flatten(it) }.single { it.name == "Crossfade" }
+        val button1 = tree1.flatMap { flatten(it) }.single { it.name == "Button" }
+        val column1 = tree1.flatMap { flatten(it) }.single { it.name == "Column" }
+        assertThat(cross1.id < RESERVED_FOR_GENERATED_IDS)
+        assertThat(button1.id < RESERVED_FOR_GENERATED_IDS)
+        assertThat(column1.id < RESERVED_FOR_GENERATED_IDS)
+
         composeTestRule.onNodeWithText("Button").performClick()
         composeTestRule.runOnIdle {
-            val second = builder.convert(androidComposeView)
-                .flatMap { flatten(it) }
-                .first { it.name == "Second" }
+            val tree2 = builder.convert(androidComposeView)
+            val second = tree2.flatMap { flatten(it) }.first { it.name == "Second" }
             assertThat(second.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
             assertThat(second.packageHash).isEqualTo(hash)
             assertThat(second.parameters.map { it.name }).contains("p2")
+
+            val cross2 = tree2.flatMap { flatten(it) }.first { it.name == "Crossfade" }
+            val button2 = tree2.flatMap { flatten(it) }.single { it.name == "Button" }
+            val column2 = tree2.flatMap { flatten(it) }.single { it.name == "Column" }
+            assertThat(cross2.id).isEqualTo(cross1.id)
+            assertThat(button2.id).isEqualTo(button1.id)
+            assertThat(column2.id).isEqualTo(column1.id)
         }
     }
 
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 5dab3dd..cb4481c 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -67,6 +67,17 @@
     packageNameHash("androidx.compose.ui.window"),
 )
 
+/**
+ * The [InspectorNode.id] will be populated with:
+ * - the layerId from a LayoutNode if this exists
+ * - an id generated from an Anchor instance from the SlotTree if this exists
+ * - a generated id if none of the above ids are available
+ *
+ * The interval -10000..-2 is reserved for the generated ids.
+ */
+@VisibleForTesting
+const val RESERVED_FOR_GENERATED_IDS = -10000L
+
 private val unwantedCalls = setOf(
     "CompositionLocalProvider",
     "Content",
@@ -387,7 +398,7 @@
         input.forEach { node ->
             if (node.name.isEmpty() && !(buildFakeChildNodes && node.layoutNodes.isNotEmpty())) {
                 parentNode.children.addAll(node.children)
-                if (node.id != UNDEFINED_ID) {
+                if (node.id > UNDEFINED_ID) {
                     // If multiple siblings with a render ids are dropped:
                     // Ignore them all. And delegate the drawing to a parent in the inspector.
                     id = if (id == null) node.id else UNDEFINED_ID
@@ -414,13 +425,13 @@
         }
         val nodeId = id
         parentNode.id =
-            if (parentNode.id == UNDEFINED_ID && nodeId != null) nodeId else parentNode.id
+            if (parentNode.id <= UNDEFINED_ID && nodeId != null) nodeId else parentNode.id
     }
 
     @OptIn(UiToolingDataApi::class)
     private fun parse(group: Group, overrideBox: IntRect? = null): MutableInspectorNode {
         val node = newNode()
-        node.id = getRenderNode(group)
+        node.id = parseId(group)
         node.name = group.name ?: ""
         parsePosition(group, node, overrideBox)
         parseLayoutInfo(group, node)
@@ -518,12 +529,19 @@
     }
 
     @OptIn(UiToolingDataApi::class)
-    private fun getRenderNode(group: Group): Long =
+    private fun parseId(group: Group): Long =
         group.modifierInfo.asSequence()
             .map { it.extra }
             .filterIsInstance<GraphicLayerInfo>()
             .map { it.layerId }
-            .firstOrNull() ?: 0
+            .firstOrNull() ?: syntheticId(group)
+
+    @OptIn(UiToolingDataApi::class)
+    private fun syntheticId(group: Group): Long {
+        val id = group.identity?.hashCode() ?: return UNDEFINED_ID
+        // The hashCode is an Int
+        return id.toLong() - Int.MAX_VALUE.toLong() + RESERVED_FOR_GENERATED_IDS
+    }
 
     private fun belongsToView(layoutNodes: List<LayoutInfo>, view: View): Boolean =
         layoutNodes.asSequence().flatMap { node ->
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
index dd3e69f..ed859db 100644
--- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.skiko.kt
@@ -270,19 +270,14 @@
         val prevBox = getBoxBackwardByOffset(offset)
         val nextBox = getBoxForwardByOffset(offset)
         return when {
-            prevBox == null -> {
-                val line = lineMetricsForOffset(offset)!!
-                return when (getParagraphDirection(offset)) {
-                    ResolvedTextDirection.Ltr -> line.left.toFloat()
-                    ResolvedTextDirection.Rtl -> line.right.toFloat()
-                }
-            }
-
-            nextBox == null || usePrimaryDirection || nextBox.direction == prevBox.direction ->
-                prevBox.cursorHorizontalPosition()
-
-            else ->
-                nextBox.cursorHorizontalPosition(true)
+            prevBox == null && nextBox == null -> 0f
+            prevBox == null -> nextBox!!.cursorHorizontalPosition(true)
+            nextBox == null -> prevBox.cursorHorizontalPosition()
+            nextBox.direction == prevBox.direction -> nextBox.cursorHorizontalPosition(true)
+            // BiDi transition offset, we need to resolve ambiguity with usePrimaryDirection
+            // for details see comment for MultiParagraph.getHorizontalPosition
+            usePrimaryDirection -> prevBox.cursorHorizontalPosition()
+            else -> nextBox.cursorHorizontalPosition(true)
         }
     }
 
diff --git a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
index df276cc..f8fb257 100644
--- a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
@@ -2,7 +2,7 @@
 package androidx.compose.ui.tooling.data {
 
   @androidx.compose.ui.tooling.data.UiToolingDataApi public final class CallGroup extends androidx.compose.ui.tooling.data.Group {
-    ctor public CallGroup(Object? key, String? name, androidx.compose.ui.unit.IntRect box, androidx.compose.ui.tooling.data.SourceLocation? location, java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters, java.util.Collection<?> data, java.util.Collection<? extends androidx.compose.ui.tooling.data.Group> children);
+    ctor public CallGroup(Object? key, String? name, androidx.compose.ui.unit.IntRect box, androidx.compose.ui.tooling.data.SourceLocation? location, Object? identity, java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters, java.util.Collection<?> data, java.util.Collection<? extends androidx.compose.ui.tooling.data.Group> children);
     property public java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters;
   }
 
@@ -10,6 +10,7 @@
     method public final androidx.compose.ui.unit.IntRect getBox();
     method public final java.util.Collection<androidx.compose.ui.tooling.data.Group> getChildren();
     method public final java.util.Collection<java.lang.Object> getData();
+    method public final Object? getIdentity();
     method public final Object? getKey();
     method public final androidx.compose.ui.tooling.data.SourceLocation? getLocation();
     method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
@@ -18,6 +19,7 @@
     property public final androidx.compose.ui.unit.IntRect box;
     property public final java.util.Collection<androidx.compose.ui.tooling.data.Group> children;
     property public final java.util.Collection<java.lang.Object> data;
+    property public final Object? identity;
     property public final Object? key;
     property public final androidx.compose.ui.tooling.data.SourceLocation? location;
     property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt b/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
index d117e47..1ef1358 100644
--- a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
+++ b/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
@@ -48,6 +48,11 @@
     val location: SourceLocation?,
 
     /**
+     * An optional value that identifies a Group independently of movement caused by recompositions.
+     */
+    val identity: Any?,
+
+    /**
      * The bounding layout box for the group.
      */
     val box: IntRect,
@@ -134,10 +139,11 @@
     name: String?,
     box: IntRect,
     location: SourceLocation?,
+    identity: Any?,
     override val parameters: List<ParameterInformation>,
     data: Collection<Any?>,
     children: Collection<Group>
-) : Group(key, name, location, box, data, children)
+) : Group(key, name, location, identity, box, data, children)
 
 /**
  * A group that represents an emitted node
@@ -154,13 +160,14 @@
     data: Collection<Any?>,
     override val modifierInfo: List<ModifierInfo>,
     children: Collection<Group>
-) : Group(key, null, null, box, data, children)
+) : Group(key, null, null, null, box, data, children)
 
 @UiToolingDataApi
 private object EmptyGroup : Group(
     key = null,
     name = null,
     location = null,
+    identity = null,
     box = emptyBox,
     data = emptyList(),
     children = emptyList()
@@ -466,6 +473,8 @@
             if (children.isEmpty()) emptyBox else
                 children.map { g -> g.box }.reduce { acc, box -> box.union(acc) }
     }
+    val location =
+        if (context?.isCall == true) { parentContext?.nextSourceLocation() } else { null }
     return if (node != null) NodeGroup(
         key,
         node,
@@ -478,8 +487,11 @@
             key,
             context?.name,
             box,
-            if (context != null && context.isCall) {
-                parentContext?.nextSourceLocation()
+            location,
+            identity = if (!context?.name.isNullOrEmpty() &&
+                            location?.sourceFile != null &&
+                            (box.bottom - box.top > 0 || box.right - box.left > 0)) {
+                this.identity
             } else {
                 null
             },
diff --git a/compose/ui/ui-tooling-preview/api/current.txt b/compose/ui/ui-tooling-preview/api/current.txt
index 241d9b7..5acb5df 100644
--- a/compose/ui/ui-tooling-preview/api/current.txt
+++ b/compose/ui/ui-tooling-preview/api/current.txt
@@ -24,6 +24,10 @@
     field public static final String PIXEL_4_XL = "id:pixel_4_xl";
     field public static final String PIXEL_C = "id:pixel_c";
     field public static final String PIXEL_XL = "id:pixel_xl";
+    field public static final String WEAR_OS_LARGE_ROUND = "id:wearos_large_round";
+    field public static final String WEAR_OS_RECT = "id:wearos_rect";
+    field public static final String WEAR_OS_SMALL_ROUND = "id:wearos_small_round";
+    field public static final String WEAR_OS_SQUARE = "id:wearos_square";
   }
 
   @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Preview {
diff --git a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
index 241d9b7..5acb5df 100644
--- a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
@@ -24,6 +24,10 @@
     field public static final String PIXEL_4_XL = "id:pixel_4_xl";
     field public static final String PIXEL_C = "id:pixel_c";
     field public static final String PIXEL_XL = "id:pixel_xl";
+    field public static final String WEAR_OS_LARGE_ROUND = "id:wearos_large_round";
+    field public static final String WEAR_OS_RECT = "id:wearos_rect";
+    field public static final String WEAR_OS_SMALL_ROUND = "id:wearos_small_round";
+    field public static final String WEAR_OS_SQUARE = "id:wearos_square";
   }
 
   @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Preview {
diff --git a/compose/ui/ui-tooling-preview/api/restricted_current.txt b/compose/ui/ui-tooling-preview/api/restricted_current.txt
index 241d9b7..5acb5df 100644
--- a/compose/ui/ui-tooling-preview/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-preview/api/restricted_current.txt
@@ -24,6 +24,10 @@
     field public static final String PIXEL_4_XL = "id:pixel_4_xl";
     field public static final String PIXEL_C = "id:pixel_c";
     field public static final String PIXEL_XL = "id:pixel_xl";
+    field public static final String WEAR_OS_LARGE_ROUND = "id:wearos_large_round";
+    field public static final String WEAR_OS_RECT = "id:wearos_rect";
+    field public static final String WEAR_OS_SMALL_ROUND = "id:wearos_small_round";
+    field public static final String WEAR_OS_SQUARE = "id:wearos_square";
   }
 
   @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Preview {
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
index 74939e4b..254acdd 100644
--- a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
@@ -45,6 +45,11 @@
     const val PIXEL_4_XL = "id:pixel_4_xl"
 
     const val AUTOMOTIVE_1024p = "id:automotive_1024p_landscape"
+
+    const val WEAR_OS_LARGE_ROUND = "id:wearos_large_round"
+    const val WEAR_OS_SMALL_ROUND = "id:wearos_small_round"
+    const val WEAR_OS_SQUARE = "id:wearos_square"
+    const val WEAR_OS_RECT = "id:wearos_rect"
 }
 
 /**
@@ -77,7 +82,12 @@
         Devices.PIXEL_4,
         Devices.PIXEL_4_XL,
 
-        Devices.AUTOMOTIVE_1024p
+        Devices.AUTOMOTIVE_1024p,
+
+        Devices.WEAR_OS_LARGE_ROUND,
+        Devices.WEAR_OS_SMALL_ROUND,
+        Devices.WEAR_OS_SQUARE,
+        Devices.WEAR_OS_RECT,
     ]
 )
 annotation class Device
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/AndroidMouse.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/AndroidMouse.kt
deleted file mode 100644
index 8b13789..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/AndroidMouse.kt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.android.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.kt
rename to compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/layout/GraphicLayerInfo.android.kt
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.android.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.kt
rename to compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAccessibilityManager.android.kt
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 5c2d443..fbec0bf 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -2504,14 +2504,16 @@
 .getAllUncoveredSemanticsNodesToMap(): Map<Int, SemanticsNodeWithAdjustedBounds> {
     val root = unmergedRootSemanticsNode
     val nodes = mutableMapOf<Int, SemanticsNodeWithAdjustedBounds>()
-    if (!root.layoutNode.isPlaced) {
+    if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) {
         return nodes
     }
     val unaccountedSpace = Region().also { it.set(root.boundsInRoot.toAndroidRect()) }
 
     fun findAllSemanticNodesRecursive(currentNode: SemanticsNode) {
+        val notAttachedOrPlaced =
+            !currentNode.layoutNode.isPlaced || !currentNode.layoutNode.isAttached
         if ((unaccountedSpace.isEmpty && currentNode.id != root.id) ||
-            (!currentNode.layoutNode.isPlaced && !currentNode.isFake)
+            (notAttachedOrPlaced && !currentNode.isFake)
         ) {
             return
         }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.android.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.kt
rename to compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/LayerMatrixCache.android.kt
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt
index b82b38c..4a870b3 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt
@@ -120,7 +120,7 @@
         get() = layer.component.transparency
         set(value) {
             if (value != layer.component.transparency) {
-                check(isUndecorated) { "Window should be undecorated!" }
+                check(isUndecorated) { "Transparent window should be undecorated!" }
                 check(!isDisplayable) {
                     "Cannot change transparency if window is already displayable."
                 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
index e0b8e92..0cc292e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
@@ -117,7 +117,7 @@
         get() = layer.component.transparency
         set(value) {
             if (value != layer.component.transparency) {
-                check(isUndecorated) { "Window should be undecorated!" }
+                check(isUndecorated) { "Transparent window should be undecorated!" }
                 check(!isDisplayable) {
                     "Cannot change transparency if window is already displayable."
                 }
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java
index b11e60c..aa06da9 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactoryTest.java
@@ -30,7 +30,7 @@
     @SmallTest
     public void testGetConverter_registeredConverter_returnsRegisteredConverter() {
         AppSearchDocumentConverter converter = AppSearchDocumentConverterFactory
-                .getConverter("Timer");
+                .getConverter("builtin:Timer");
 
         assertThat(converter).isNotNull();
         assertThat(converter).isInstanceOf(TimerConverter.class);
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
index 22b8a02..8607bcf 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.when;
+
 import android.content.Context;
 import android.provider.AlarmClock;
 
@@ -29,39 +31,52 @@
 
 import com.google.firebase.appindexing.Indexable;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.TimeZone;
 
 @RunWith(AndroidJUnit4.class)
 public class TimerConverterTest {
-    private final TimerConverter mTimerConverter = new TimerConverter();
+    @Mock private TimeModel mTimeModelMock;
+    private TimerConverter mTimerConverter;
     private final Context mContext = ApplicationProvider.getApplicationContext();
 
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mTimerConverter = new TimerConverter(mTimeModelMock);
+    }
+
     @Test
     @SmallTest
     public void testConvert_returnsIndexable() throws Exception {
         // Expire time is timezone sensitive. Force the default timezone to be GMT here.
         TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
 
+        // mock 5 seconds passed since the timer startTime
+        when(mTimeModelMock.getSystemCurrentTimeMillis()).thenReturn(8000L);
+        when(mTimeModelMock.getSystemClockElapsedRealtime()).thenReturn(10000L);
+
         Timer timer = new Timer.Builder("namespace", "id")
-                .setDurationMillis(1000)
-                .setTimerStatus(Timer.STATUS_STARTED)
-                .setExpireTimeMillis(3000)
-                .setName("my timer")
-                .setRemainingTimeMillis(500)
-                .setRingtone(AlarmClock.VALUE_RINGTONE_SILENT)
                 .setScore(10)
                 .setTtlMillis(60000)
+                .setCreationTimestampMillis(1)
+                .setName("my timer")
+                .setDurationMillis(1000)
+                .setRemainingTimeMillis(10000)
+                .setRingtone(AlarmClock.VALUE_RINGTONE_SILENT)
+                .setStatus(Timer.STATUS_STARTED)
                 .setVibrate(true)
+                .setStartTimeMillis(3000)
+                .setStartTimeMillisInElapsedRealtime(5000)
                 .build();
 
-        Indexable result = mTimerConverter.convertGenericDocument(mContext,
-                GenericDocument.fromDocumentClass(timer))
-                // Override creation timestamp to a constant instead of System.currentTimeMillis.
-                // TODO: add creation timestamp to timer.
-                .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
+        Indexable result = mTimerConverter.convertGenericDocument(
+                mContext, GenericDocument.fromDocumentClass(timer))
                 .build();
         Indexable expectedResult = new Indexable.Builder("Timer")
                 .setMetadata(new Indexable.Metadata.Builder().setScore(10))
@@ -73,11 +88,69 @@
                 .put(IndexableKeys.NAMESPACE, "namespace")
                 .put(IndexableKeys.TTL_MILLIS, 60000)
                 .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
+                .put("message", "my timer")
                 .put("length", 1000)
                 .put("timerStatus", "Started")
-                .put("expireTime", "1970-01-01T00:00:03+0000")
+                // Calculation: 3000 + 10000 = 13000 = 13 seconds since epoch
+                .put("expireTime", "1970-01-01T00:00:13+0000")
+                // Calculation: 8000 - (10000 - 5000) + 10000 = 13000 = 13 seconds since epoch
+                .put("expireTimeCorrectedByStartTimeInElapsedRealtime",
+                        "1970-01-01T00:00:13+0000")
+                .put("remainingTime", 10000)
+                .put("ringtone", "silent")
+                .put("vibrate", true)
+                .build();
+        assertThat(result).isEqualTo(expectedResult);
+    }
+
+    @Test
+    @SmallTest
+    public void testConvert_differentSystemCurrentTime_returnsIndexable() throws Exception {
+        // Expire time is timezone sensitive. Force the default timezone to be GMT here.
+        TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
+
+        // 10 seconds has passed for System.CurrentTimeMillis
+        when(mTimeModelMock.getSystemCurrentTimeMillis()).thenReturn(13000L);
+        // 5 seconds has passed for SystemClock.elapsedRealtime
+        when(mTimeModelMock.getSystemClockElapsedRealtime()).thenReturn(10000L);
+
+        Timer timer = new Timer.Builder("namespace", "id")
+                .setScore(10)
+                .setTtlMillis(60000)
+                .setCreationTimestampMillis(1)
+                .setName("my timer")
+                .setDurationMillis(1000)
+                .setRemainingTimeMillis(10000)
+                .setRingtone(AlarmClock.VALUE_RINGTONE_SILENT)
+                .setStatus(Timer.STATUS_STARTED)
+                .setVibrate(true)
+                .setStartTimeMillis(3000)
+                .setStartTimeMillisInElapsedRealtime(5000)
+                .build();
+
+        Indexable result = mTimerConverter.convertGenericDocument(
+                mContext, GenericDocument.fromDocumentClass(timer))
+                .build();
+        Indexable expectedResult = new Indexable.Builder("Timer")
+                .setMetadata(new Indexable.Metadata.Builder().setScore(10))
+                .setId("id")
+                .setName("namespace")
+                .setUrl("intent:#Intent;action=androidx.core.content.pm.SHORTCUT_LISTENER;"
+                        + "component=androidx.core.google.shortcuts.test/androidx.core.google."
+                        + "shortcuts.TrampolineActivity;S.id=id;end")
+                .put(IndexableKeys.NAMESPACE, "namespace")
+                .put(IndexableKeys.TTL_MILLIS, 60000)
+                .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
                 .put("message", "my timer")
-                .put("remainingTime", 500)
+                .put("length", 1000)
+                .put("timerStatus", "Started")
+                // Calculation: 3000 + 10000 = 13000 = 13 seconds since epoch
+                .put("expireTime", "1970-01-01T00:00:13+0000")
+                // Calculation: 13000 - (10000 - 5000) + 10000 = 18000 = 18 seconds since epoch
+                // This is the more correct expire time
+                .put("expireTimeCorrectedByStartTimeInElapsedRealtime",
+                        "1970-01-01T00:00:18+0000")
+                .put("remainingTime", 10000)
                 .put("ringtone", "silent")
                 .put("vibrate", true)
                 .build();
@@ -88,13 +161,12 @@
     @SmallTest
     public void testConvert_withoutOptionalFields_returnsIndexable() throws Exception {
         Timer timer = new Timer.Builder("namespace", "id")
+                // need to override to a value, otherwise it will use current time
+                .setCreationTimestampMillis(0)
                 .build();
 
         Indexable result = mTimerConverter.convertGenericDocument(mContext,
                 GenericDocument.fromDocumentClass(timer))
-                // Override creation timestamp to a constant instead of System.currentTimeMillis.
-                // TODO: add creation timestamp to timer.
-                .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
                 .build();
         Indexable expectedResult = new Indexable.Builder("Timer")
                 .setMetadata(new Indexable.Metadata.Builder().setScore(0))
@@ -105,7 +177,7 @@
                         + "shortcuts.TrampolineActivity;S.id=id;end")
                 .put(IndexableKeys.NAMESPACE, "namespace")
                 .put(IndexableKeys.TTL_MILLIS, 0)
-                .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1)
+                .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 0)
                 .put("length", 0)
                 .put("timerStatus", "Unknown")
                 .put("remainingTime", 0)
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
index e5cc3b4..ad5592f 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
@@ -55,6 +55,7 @@
                 .put(IndexableKeys.TTL_MILLIS, 0)
                 .put(IndexableKeys.CREATION_TIMESTAMP_MILLIS, 1);
         assertThat(ConverterUtils.buildBaseIndexableFromGenericDocument(
-                mContext, genericDocument).build()).isEqualTo(expectedIndexableBuilder.build());
+                mContext, "schema", genericDocument).build())
+                .isEqualTo(expectedIndexableBuilder.build());
     }
 }
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java
index c4136ad..32b5af0 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverterFactory.java
@@ -35,6 +35,8 @@
 public class AppSearchDocumentConverterFactory {
     private static final String TAG = "AppSearchDocumentConver"; // NOTYPO
 
+    private static final String TIMER_SCHEMA_TYPE = "builtin:Timer";
+
     /**
      * Returns a {@link AppSearchDocumentConverter} given a schema type. If the schema type is not
      * supported, then the {@link GenericDocumentConverter} will be returned.
@@ -43,7 +45,7 @@
     public static AppSearchDocumentConverter getConverter(@NonNull String schemaType) {
         Preconditions.checkNotNull(schemaType);
 
-        if ("Timer".equals(schemaType)) {
+        if (TIMER_SCHEMA_TYPE.equals(schemaType)) {
             return new TimerConverter();
         }
 
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
index 0e15087..0c25217 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
@@ -50,7 +50,7 @@
         Preconditions.checkNotNull(genericDocument);
 
         Indexable.Builder indexableBuilder = ConverterUtils.buildBaseIndexableFromGenericDocument(
-                context, genericDocument);
+                context, genericDocument.getSchemaType(), genericDocument);
 
         for (String property : genericDocument.getPropertyNames()) {
             Object rawProperty = genericDocument.getProperty(property);
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimeModel.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimeModel.java
new file mode 100644
index 0000000..12256a3
--- /dev/null
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimeModel.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.core.google.shortcuts.converters;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.os.SystemClock;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Wrapper around time related system methods. This makes unit testing against system times
+ * easier.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class TimeModel {
+    /** Returns {@link System#currentTimeMillis()}. */
+    public long getSystemCurrentTimeMillis() {
+        return System.currentTimeMillis();
+    }
+
+    /** Returns {@link SystemClock#elapsedRealtime()}. */
+    public long getSystemClockElapsedRealtime() {
+        return SystemClock.elapsedRealtime();
+    }
+}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
index b09f04a..608ec9d 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
@@ -23,6 +23,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.builtintypes.Timer;
 import androidx.core.google.shortcuts.utils.ConverterUtils;
@@ -49,15 +50,20 @@
     private static final String DURATION_MILLIS_KEY = "durationMillis";
     private static final String REMAINING_TIME_MILLIS_KEY = "remainingTimeMillis";
     private static final String RINGTONE_KEY = "ringtone";
+    private static final String STATUS_KEY = "status";
     private static final String VIBRATE_KEY = "vibrate";
-    private static final String TIMER_STATUS_KEY = "timerStatus";
-    private static final String EXPIRE_TIME_MILLIS_KEY = "expireTimeMillis";
+    private static final String START_TIME_MILLIS_KEY = "startTimeMillis";
+    private static final String START_TIME_MILLIS_IN_ELAPSED_REALTIME_KEY =
+            "startTimeMillisInElapsedRealtime";
 
     // Keys for Indexables
     private static final String MESSAGE_KEY = "message";
     private static final String LENGTH_KEY = "length";
     private static final String REMAINING_TIME_KEY = "remainingTime";
     private static final String EXPIRE_TIME_KEY = "expireTime";
+    private static final String EXPIRE_TIME_CORRECTED_BY_START_TIME_IN_ELAPSED_REALTIME_KEY =
+            "expireTimeCorrectedByStartTimeInElapsedRealtime";
+    private static final String TIMER_STATUS_KEY = "timerStatus";
 
     // Enums for TimerStatus
     private static final String STARTED = "Started";
@@ -67,6 +73,8 @@
     private static final String RESET = "Reset";
     private static final String UNKNOWN = "Unknown";
 
+    private static final String TIMER_INDEXABLE_TYPE = "Timer";
+
     private static final ThreadLocal<DateFormat> ISO8601_DATE_TIME_FORMAT =
             new ThreadLocal<DateFormat>() {
         @Override
@@ -75,6 +83,17 @@
         }
     };
 
+    private final TimeModel mTimeModel;
+
+    public TimerConverter() {
+        this(new TimeModel());
+    }
+
+    @VisibleForTesting
+    TimerConverter(TimeModel timeModel) {
+        mTimeModel = timeModel;
+    }
+
     @Override
     @NonNull
     public Indexable.Builder convertGenericDocument(@NonNull Context context,
@@ -83,7 +102,7 @@
         Preconditions.checkNotNull(timer);
 
         Indexable.Builder indexableBuilder = ConverterUtils.buildBaseIndexableFromGenericDocument(
-                    context, timer);
+                    context, TIMER_INDEXABLE_TYPE, timer);
 
         indexableBuilder
                 .put(MESSAGE_KEY, timer.getPropertyString(NAME_KEY))
@@ -92,7 +111,7 @@
                 .put(RINGTONE_KEY, timer.getPropertyString(RINGTONE_KEY))
                 .put(VIBRATE_KEY, timer.getPropertyBoolean(VIBRATE_KEY));
 
-        int timerStatus = (int) timer.getPropertyLong(TIMER_STATUS_KEY);
+        int timerStatus = (int) timer.getPropertyLong(STATUS_KEY);
         switch (timerStatus) {
             case Timer.STATUS_UNKNOWN:
                 indexableBuilder.put(TIMER_STATUS_KEY, UNKNOWN);
@@ -114,17 +133,35 @@
                 break;
             default:
                 indexableBuilder.put(TIMER_STATUS_KEY, UNKNOWN);
-                Log.w(TAG, "Invalud time status: " + timerStatus + ", defaulting to "
+                Log.w(TAG, "Invalid time status: " + timerStatus + ", defaulting to "
                         + "Timer.STATUS_UNKNOWN");
         }
 
-        // 0 means never expire.
-        long expireTime = timer.getPropertyLong(EXPIRE_TIME_MILLIS_KEY);
-        if (expireTime > 0) {
-            Date date = new Date(expireTime);
-            indexableBuilder.put(EXPIRE_TIME_KEY,
-                    Preconditions.checkNotNull(ISO8601_DATE_TIME_FORMAT.get()).format(date));
+        if (timerStatus == Timer.STATUS_STARTED) {
+            long startTime = timer.getPropertyLong(START_TIME_MILLIS_KEY);
+            long remainingTime = timer.getPropertyLong(REMAINING_TIME_MILLIS_KEY);
+
+            long expireTime = remainingTime + startTime;
+            indexableBuilder.put(EXPIRE_TIME_KEY, convertTimeToISOFormat(expireTime));
+
+            long startTimeInElapsedRealtime =
+                    timer.getPropertyLong(START_TIME_MILLIS_IN_ELAPSED_REALTIME_KEY);
+            if (startTimeInElapsedRealtime >= 0) {
+                // If startTime in elapsed realtime is set, use that to calculate expire time as
+                // well.
+                long elapsedTime = mTimeModel.getSystemClockElapsedRealtime()
+                        - startTimeInElapsedRealtime;
+                long expireTimeFromElapsedRealtime =
+                        mTimeModel.getSystemCurrentTimeMillis() - elapsedTime + remainingTime;
+                indexableBuilder.put(EXPIRE_TIME_CORRECTED_BY_START_TIME_IN_ELAPSED_REALTIME_KEY,
+                        convertTimeToISOFormat(expireTimeFromElapsedRealtime));
+            }
         }
         return indexableBuilder;
     }
+
+    private String convertTimeToISOFormat(long time) {
+        Date date = new Date(time);
+        return Preconditions.checkNotNull(ISO8601_DATE_TIME_FORMAT.get()).format(date);
+    }
 }
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
index 35a05d9..f67c5a9 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
@@ -40,11 +40,12 @@
     @NonNull
     public static Indexable.Builder buildBaseIndexableFromGenericDocument(
             @NonNull Context context,
+            @NonNull String indexableType,
             @NonNull GenericDocument genericDocument) {
         Preconditions.checkNotNull(context);
         Preconditions.checkNotNull(genericDocument);
 
-        return new Indexable.Builder(genericDocument.getSchemaType())
+        return new Indexable.Builder(indexableType)
                 .setId(genericDocument.getId())
                 // TODO (b/206020715): remove name when it's no longer a required field.
                 .setName(genericDocument.getNamespace())
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 9284f90..910aa2e 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -223,7 +223,7 @@
 \^
 symbol\:   static FLAG_MUTABLE
 location\: class PendingIntent
-\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
+\$OUT_DIR\/androidx\/docs\-public\/build\/srcs\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
 # > Task :buildOnServer
 [0-9]+ problems were found reusing the configuration cache\.
 [0-9]+ problems were found reusing the configuration cache, [0-9]+ of which seem unique\.
@@ -417,7 +417,7 @@
 # > Task :compose:ui:ui-graphics:ui-graphics-benchmark:processReleaseAndroidTestManifest
 \$SUPPORT/compose/ui/ui\-graphics/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
 # > Task :docs-public:doclavaDocs
-\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/WorkManagerImpl\.java\:[0-9]+\: error\: cannot find symbol
+\$OUT_DIR\/androidx\/docs\-public\/build\/srcs\/androidx\/work\/impl\/WorkManagerImpl\.java\:[0-9]+\: error\: cannot find symbol
 import static android\.app\.PendingIntent\.FLAG_MUTABLE\;
 # > Task :docs-public:dackkaDocs
 PROGRESS: Initializing plugins
@@ -612,4 +612,4 @@
 Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint OBJECT\.
 Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
 # > Task :compose:ui:ui-inspection:buildCMakeRelWithDebInfo[arm64-v8a]
-C/C\+\+: ninja: warning: bad deps log signature or version; starting over
\ No newline at end of file
+C/C\+\+: ninja: warning: bad deps log signature or version; starting over
diff --git a/development/update_studio.sh b/development/update_studio.sh
index 1a40e5b..235344c 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 # Get versions
-AGP_VERSION=${1:-7.1.0-beta03}
-STUDIO_VERSION_STRING=${2:-"Android Studio Bumblebee (2021.1.1) Beta 3"}
+AGP_VERSION=${1:-7.2.0-alpha06}
+STUDIO_VERSION_STRING=${2:-"Android Studio Chipmunk (2021.2.1) Canary 6"}
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep iframe | sed "s/.*src=\"\([a-zA-Z0-9\/\._]*\)\".*/https:\/\/android-dot-devsite-v2-prod.appspot.com\1/g"`
 STUDIO_LINK=`curl -s $STUDIO_IFRAME_LINK | grep -C30 "$STUDIO_VERSION_STRING" | grep Linux | tail -n 1 | sed 's/.*a href="\(.*\).*"/\1/g'`
 STUDIO_VERSION=`echo $STUDIO_LINK | sed "s/.*ide-zips\/\(.*\)\/android-studio-.*/\1/g"`
@@ -39,7 +39,7 @@
     ./development/importMaven/import_maven_artifacts.py -n "com.android.tools.utp:$ARTIFACT:$ADT_VERSION"
   done
 
-ATP_VERSION=${4:-0.0.8-alpha06}
+ATP_VERSION=${4:-0.0.8-alpha07}
 ./development/importMaven/import_maven_artifacts.py -n "com.google.testing.platform:android-test-plugin:$ATP_VERSION"
 ./development/importMaven/import_maven_artifacts.py -n "com.google.testing.platform:launcher:$ATP_VERSION"
 ./development/importMaven/import_maven_artifacts.py -n "com.google.testing.platform:android-driver-instrumentation:$ATP_VERSION"
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ee404c5..5032588 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -23,10 +23,10 @@
     docs("androidx.arch.core:core-testing:2.1.0")
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.0.0")
     docs("androidx.autofill:autofill:1.2.0-beta01")
-    docs("androidx.benchmark:benchmark-common:1.1.0-beta01")
-    docs("androidx.benchmark:benchmark-junit4:1.1.0-beta01")
-    docs("androidx.benchmark:benchmark-macro:1.1.0-beta01")
-    docs("androidx.benchmark:benchmark-macro-junit4:1.1.0-beta01")
+    docs("androidx.benchmark:benchmark-common:1.1.0-alpha13")
+    docs("androidx.benchmark:benchmark-junit4:1.1.0-alpha13")
+    docs("androidx.benchmark:benchmark-macro:1.1.0-alpha13")
+    docs("androidx.benchmark:benchmark-macro-junit4:1.1.0-alpha13")
     docs("androidx.biometric:biometric:1.2.0-alpha04")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha04")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha04")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index bf20035..a22fca0 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -265,6 +265,8 @@
     samples(project(":wear:compose:compose-foundation-samples"))
     docs(project(":wear:compose:compose-material"))
     samples(project(":wear:compose:compose-material-samples"))
+    docs(project(":wear:compose:compose-navigation"))
+    samples(project(":wear:compose:compose-navigation-samples"))
     docs(project(":wear:wear-input"))
     docs(project(":wear:wear-input-testing"))
     samples(project(":wear:wear-input-samples"))
@@ -279,6 +281,7 @@
     docs(project(":wear:watchface:watchface"))
     docs(project(":wear:watchface:watchface-complications"))
     docs(project(":wear:watchface:watchface-complications-data"))
+    docs(project(":wear:watchface:watchface-complications-data-core"))
     docs(project(":wear:watchface:watchface-complications-data-source"))
     samples(project(":wear:watchface:watchface-complications-data-source-samples"))
     docs(project(":wear:watchface:watchface-complications-rendering"))
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6bcb125..c5b17de 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,20 +2,20 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "7.1.0-beta03"
+androidGradlePlugin = "7.2.0-alpha06"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "30.1.0-beta03"
+androidLint = "30.2.0-alpha06"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2021.1.1.16"
+androidStudio = "2021.2.1.6"
 # -----------------------------------------------------------------------------
 
 androidLintMin = "27.2.1"
 androidLintMinCompose = "30.0.0"
 androidxTestRunner = "1.4.0"
 androidxTestRules = "1.4.0"
-androidxTestMonitor = "1.5.0-rc01"
+androidxTestMonitor = "1.5.0"
 androidxTestCore = "1.4.0"
 androidxTestExtJunit = "1.1.3"
 androidxTestExtTruth = "1.4.0"
@@ -28,10 +28,10 @@
 guavaJre = "29.0-jre"
 hilt = "2.40.1"
 incap = "0.2"
-kotlin = "1.6.0"
+kotlin = "1.6.10"
 kotlinCompileTesting = "1.4.1"
 kotlinCoroutines = "1.5.2"
-ksp = "1.6.0-1.0.1"
+ksp = "1.6.10-1.0.2"
 ktlint = "0.43.0"
 leakcanary = "2.7"
 mockito = "2.25.0"
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index ae9f741..35cac7a 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -39,10 +39,6 @@
       "to": "ignore"
     },
     {
-      "from": "android/support/compat/R(.*)",
-      "to": "androidx/core/R{0}"
-    },
-    {
       "from": "android/support/v7/recyclerview/R(.*)",
       "to": "androidx/recyclerview/R{0}"
     },
@@ -183,22 +179,6 @@
       "to": "androidx/recyclerview/widget/{0}"
     },
     {
-      "from": "android/support/v4/content/res/GrowingArrayUtils(.*)",
-      "to": "androidx/core/content/res/GrowingArrayUtils{0}"
-    },
-    {
-      "from": "android/support/v7/content/res/GrowingArrayUtils(.*)",
-      "to": "androidx/core/content/res/GrowingArrayUtils{0}"
-    },
-    {
-      "from": "android/support/v4/content/res/ColorStateListInflaterCompat(.*)",
-      "to": "androidx/core/content/res/ColorStateListInflaterCompat{0}"
-    },
-    {
-      "from": "android/support/v7/content/res/AppCompatColorStateListInflater(.*)",
-      "to": "androidx/core/content/res/ColorStateListInflaterCompat{0}"
-    },
-    {
       "from": "android/support/v7/(.*)",
       "to": "androidx/appcompat/{0}"
     },
@@ -207,14 +187,6 @@
       "to": "androidx/annotation/{0}"
     },
     {
-      "from": "android/support/v4/view/animation/PathInterpolatorApi14(.*)",
-      "to": "androidx/core/view/animation/PathInterpolatorApi14{0}"
-    },
-    {
-      "from": "android/support/v4/view/animation/PathInterpolatorCompat(.*)",
-      "to": "androidx/core/view/animation/PathInterpolatorCompat{0}"
-    },
-    {
       "from": "android/support/v4/app/SupportActivity(.*)",
       "to": "androidx/core/app/ComponentActivity{0}"
     },
@@ -305,15 +277,15 @@
       "to": "ignore"
     },
     {
-      "from": "androidx/core/content/ContextCompat(.*)",
+      "from": "androidx/core/database/(.*)",
       "to": "ignore"
     },
     {
-      "from": "androidx/core/content/UnusedAppRestrictionsBackportCallback(.*)",
+      "from": "androidx/core/internal/view/(.*)",
       "to": "ignore"
     },
     {
-      "from": "androidx/core/content/UnusedAppRestrictionsBackportService(.*)",
+      "from": "androidx/core/content/(.*)",
       "to": "ignore"
     },
     {
@@ -349,6 +321,10 @@
       "to": "ignore"
     },
     {
+      "from": "androidx/core/view/(.*)",
+      "to": "ignore"
+    },
+    {
       "from": "androidx/core/accessibilityservice/AccessibilityServiceInfoCompat(.*)",
       "to": "ignore"
     },
@@ -389,6 +365,94 @@
       "to": "ignore"
     },
     {
+      "from": "androidx/core/app/NavUtils(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationChannelCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationCompatSideChannelService(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationManagerCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/RemoteInput(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/RemoteActionCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationBuilderWithBuilderAccessor(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationCompatBuilder(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationCompatExtras(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/NotificationCompatJellybean(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/FrameMetricsAggregator(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/telephony/(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/R(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/DialogCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/BundleCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/ServiceCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/ShareCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/SharedElementCallback(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/app/TaskStackBuilder(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/hardware/(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/location/(.*)",
+      "to": "ignore"
+    },
+    {
       "from": "androidx/core/os/BuildCompat(.*)",
       "to": "ignore"
     },
@@ -448,11 +512,35 @@
       "from": "androidx/core/os/TraceCompat(.*)",
       "to": "ignore"
     },
+     {
+      "from": "androidx/core/os/ExecutorCompat(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/os/UserHandleCompat(.*)",
+      "to": "ignore"
+    },
     {
       "from": "androidx/core/os/UserManagerCompat(.*)",
       "to": "ignore"
     },
     {
+      "from": "androidx/core/splashscreen(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/graphics/(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/util/(.*)",
+      "to": "ignore"
+    },
+    {
+      "from": "androidx/core/widget/(.*)",
+      "to": "ignore"
+    },
+    {
       "from": "androidx/viewpager2/(.*)",
       "to": "ignore"
     },
@@ -1227,7 +1315,7 @@
       "to": "androidx/leanback/tab"
     },
     {
-      "from": "android/support/compat",
+      "from": "androidx/core",
       "to": "androidx/core"
     },
     {
@@ -2130,9 +2218,9 @@
     },
     {
       "from": {
-        "groupId": "com.android.support",
-        "artifactId": "support-compat",
-        "version": "{oldSlVersion}"
+        "groupId": "androidx.core",
+        "artifactId": "core",
+        "version": "{newSlVersion}"
       },
       "to": {
         "groupId": "androidx.core",
@@ -3986,7 +4074,6 @@
       "android/support/annotation/VisibleForTesting": "androidx/annotation/VisibleForTesting",
       "android/support/annotation/WorkerThread": "androidx/annotation/WorkerThread",
       "android/support/annotation/XmlRes": "androidx/annotation/XmlRes",
-      "android/support/compat/R": "androidx/core/R",
       "android/support/constraint/Barrier": "androidx/constraintlayout/widget/Barrier",
       "android/support/constraint/ConstraintHelper": "androidx/constraintlayout/widget/ConstraintHelper",
       "android/support/constraint/ConstraintLayout": "androidx/constraintlayout/widget/ConstraintLayout",
@@ -4029,7 +4116,6 @@
       "android/support/multidex/ZipUtil": "androidx/multidex/ZipUtil",
       "android/support/v4/app/BackStackRecord": "androidx/fragment/app/BackStackRecord",
       "android/support/v4/app/BackStackState": "androidx/fragment/app/BackStackState",
-      "android/support/v4/app/BundleCompat": "androidx/core/app/BundleCompat",
       "android/support/v4/app/DialogFragment": "androidx/fragment/app/DialogFragment",
       "android/support/v4/app/Fragment": "androidx/fragment/app/Fragment",
       "android/support/v4/app/FragmentActivity": "androidx/fragment/app/FragmentActivity",
@@ -4048,146 +4134,14 @@
       "android/support/v4/app/FragmentTransition": "androidx/fragment/app/FragmentTransition",
       "android/support/v4/app/FragmentTransitionCompat21": "androidx/fragment/app/FragmentTransitionCompat21",
       "android/support/v4/app/FragmentTransitionImpl": "androidx/fragment/app/FragmentTransitionImpl",
-      "android/support/v4/app/FrameMetricsAggregator": "androidx/core/app/FrameMetricsAggregator",
       "android/support/v4/app/INotificationSideChannel": "androidx/core/app/INotificationSideChannel",
       "android/support/v4/app/ListFragment": "androidx/fragment/app/ListFragment",
-      "android/support/v4/app/NavUtils": "androidx/core/app/NavUtils",
-      "android/support/v4/app/NotificationBuilderWithBuilderAccessor": "androidx/core/app/NotificationBuilderWithBuilderAccessor",
-      "android/support/v4/app/NotificationCompat": "androidx/core/app/NotificationCompat",
-      "android/support/v4/app/NotificationCompatBuilder": "androidx/core/app/NotificationCompatBuilder",
-      "android/support/v4/app/NotificationCompatExtras": "androidx/core/app/NotificationCompatExtras",
-      "android/support/v4/app/NotificationCompatJellybean": "androidx/core/app/NotificationCompatJellybean",
-      "android/support/v4/app/NotificationCompatSideChannelService": "androidx/core/app/NotificationCompatSideChannelService",
-      "android/support/v4/app/NotificationManagerCompat": "androidx/core/app/NotificationManagerCompat",
       "android/support/v4/app/OneShotPreDrawListener": "androidx/fragment/app/OneShotPreDrawListener",
-      "android/support/v4/app/RemoteInput": "androidx/core/app/RemoteInput",
-      "android/support/v4/app/ServiceCompat": "androidx/core/app/ServiceCompat",
-      "android/support/v4/app/ShareCompat": "androidx/core/app/ShareCompat",
-      "android/support/v4/app/SharedElementCallback": "androidx/core/app/SharedElementCallback",
       "android/support/v4/app/SuperNotCalledException": "androidx/fragment/app/SuperNotCalledException",
       "android/support/v4/app/SupportActivity": "androidx/core/app/ComponentActivity",
-      "android/support/v4/app/TaskStackBuilder": "androidx/core/app/TaskStackBuilder",
-      "android/support/v4/content/ContentResolverCompat": "androidx/core/content/ContentResolverCompat",
-      "android/support/v4/content/FileProvider": "androidx/core/content/FileProvider",
-      "android/support/v4/content/IntentCompat": "androidx/core/content/IntentCompat",
-      "android/support/v4/content/MimeTypeFilter": "androidx/core/content/MimeTypeFilter",
-      "android/support/v4/content/PermissionChecker": "androidx/core/content/PermissionChecker",
-      "android/support/v4/content/SharedPreferencesCompat": "androidx/core/content/SharedPreferencesCompat",
-      "android/support/v4/content/pm/ActivityInfoCompat": "androidx/core/content/pm/ActivityInfoCompat",
-      "android/support/v4/content/pm/PackageInfoCompat": "androidx/core/content/pm/PackageInfoCompat",
-      "android/support/v4/content/pm/PermissionInfoCompat": "androidx/core/content/pm/PermissionInfoCompat",
-      "android/support/v4/content/pm/ShortcutInfoCompat": "androidx/core/content/pm/ShortcutInfoCompat",
-      "android/support/v4/content/pm/ShortcutManagerCompat": "androidx/core/content/pm/ShortcutManagerCompat",
-      "android/support/v4/content/res/ColorStateListInflaterCompat": "androidx/core/content/res/ColorStateListInflaterCompat",
-      "android/support/v4/content/res/ComplexColorCompat": "androidx/core/content/res/ComplexColorCompat",
-      "android/support/v4/content/res/ConfigurationHelper": "androidx/core/content/res/ConfigurationHelper",
-      "android/support/v4/content/res/FontResourcesParserCompat": "androidx/core/content/res/FontResourcesParserCompat",
-      "android/support/v4/content/res/GradientColorInflaterCompat": "androidx/core/content/res/GradientColorInflaterCompat",
-      "android/support/v4/content/res/GrowingArrayUtils": "androidx/core/content/res/GrowingArrayUtils",
-      "android/support/v4/content/res/ResourcesCompat": "androidx/core/content/res/ResourcesCompat",
-      "android/support/v4/content/res/TypedArrayUtils": "androidx/core/content/res/TypedArrayUtils",
-      "android/support/v4/database/CursorWindowCompat": "androidx/core/database/CursorWindowCompat",
-      "android/support/v4/database/DatabaseUtilsCompat": "androidx/core/database/DatabaseUtilsCompat",
-      "android/support/v4/database/sqlite/SQLiteCursorCompat": "androidx/core/database/sqlite/SQLiteCursorCompat",
-      "android/support/v4/graphics/BitmapCompat": "androidx/core/graphics/BitmapCompat",
-      "android/support/v4/graphics/ColorUtils": "androidx/core/graphics/ColorUtils",
-      "android/support/v4/graphics/PaintCompat": "androidx/core/graphics/PaintCompat",
-      "android/support/v4/graphics/PathParser": "androidx/core/graphics/PathParser",
-      "android/support/v4/graphics/PathSegment": "androidx/core/graphics/PathSegment",
-      "android/support/v4/graphics/PathUtils": "androidx/core/graphics/PathUtils",
-      "android/support/v4/graphics/TypefaceCompat": "androidx/core/graphics/TypefaceCompat",
-      "android/support/v4/graphics/TypefaceCompatApi21Impl": "androidx/core/graphics/TypefaceCompatApi21Impl",
-      "android/support/v4/graphics/TypefaceCompatApi24Impl": "androidx/core/graphics/TypefaceCompatApi24Impl",
-      "android/support/v4/graphics/TypefaceCompatApi26Impl": "androidx/core/graphics/TypefaceCompatApi26Impl",
-      "android/support/v4/graphics/TypefaceCompatApi28Impl": "androidx/core/graphics/TypefaceCompatApi28Impl",
-      "android/support/v4/graphics/TypefaceCompatBaseImpl": "androidx/core/graphics/TypefaceCompatBaseImpl",
-      "android/support/v4/graphics/TypefaceCompatUtil": "androidx/core/graphics/TypefaceCompatUtil",
-      "android/support/v4/graphics/drawable/DrawableCompat": "androidx/core/graphics/drawable/DrawableCompat",
-      "android/support/v4/graphics/drawable/IconCompat": "androidx/core/graphics/drawable/IconCompat",
       "android/support/v4/graphics/drawable/IconCompatParcelizer": "android/support/v4/graphics/drawable/IconCompatParcelizer",
-      "android/support/v4/graphics/drawable/RoundedBitmapDrawable": "androidx/core/graphics/drawable/RoundedBitmapDrawable",
-      "android/support/v4/graphics/drawable/RoundedBitmapDrawable21": "androidx/core/graphics/drawable/RoundedBitmapDrawable21",
-      "android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory": "androidx/core/graphics/drawable/RoundedBitmapDrawableFactory",
-      "android/support/v4/graphics/drawable/TintAwareDrawable": "androidx/core/graphics/drawable/TintAwareDrawable",
-      "android/support/v4/graphics/drawable/WrappedDrawable": "androidx/core/graphics/drawable/WrappedDrawable",
-      "android/support/v4/graphics/drawable/WrappedDrawableApi14": "androidx/core/graphics/drawable/WrappedDrawableApi14",
-      "android/support/v4/graphics/drawable/WrappedDrawableApi21": "androidx/core/graphics/drawable/WrappedDrawableApi21",
-      "android/support/v4/hardware/display/DisplayManagerCompat": "androidx/core/hardware/display/DisplayManagerCompat",
-      "android/support/v4/hardware/fingerprint/FingerprintManagerCompat": "androidx/core/hardware/fingerprint/FingerprintManagerCompat",
-      "android/support/v4/internal/view/SupportMenu": "androidx/core/internal/view/SupportMenu",
-      "android/support/v4/internal/view/SupportMenuItem": "androidx/core/internal/view/SupportMenuItem",
-      "android/support/v4/internal/view/SupportSubMenu": "androidx/core/internal/view/SupportSubMenu",
       "android/support/v4/os/IResultReceiver": "androidx/core/os/IResultReceiver",
       "android/support/v4/os/ResultReceiver": "androidx/core/os/ResultReceiver",
-      "android/support/v4/util/AtomicFile": "androidx/core/util/AtomicFile",
-      "android/support/v4/util/Consumer": "androidx/core/util/Consumer",
-      "android/support/v4/util/DebugUtils": "androidx/core/util/DebugUtils",
-      "android/support/v4/util/LogWriter": "androidx/core/util/LogWriter",
-      "android/support/v4/util/ObjectsCompat": "androidx/core/util/ObjectsCompat",
-      "android/support/v4/util/Pair": "androidx/core/util/Pair",
-      "android/support/v4/util/PatternsCompat": "androidx/core/util/PatternsCompat",
-      "android/support/v4/util/Pools": "androidx/core/util/Pools",
-      "android/support/v4/util/Preconditions": "androidx/core/util/Preconditions",
-      "android/support/v4/util/TimeUtils": "androidx/core/util/TimeUtils",
-      "android/support/v4/view/AccessibilityDelegateCompat": "androidx/core/view/AccessibilityDelegateCompat",
-      "android/support/v4/view/ActionProvider": "androidx/core/view/ActionProvider",
-      "android/support/v4/view/DisplayCutoutCompat": "androidx/core/view/DisplayCutoutCompat",
-      "android/support/v4/view/GestureDetectorCompat": "androidx/core/view/GestureDetectorCompat",
-      "android/support/v4/view/GravityCompat": "androidx/core/view/GravityCompat",
-      "android/support/v4/view/InputDeviceCompat": "androidx/core/view/InputDeviceCompat",
-      "android/support/v4/view/KeyEventDispatcher": "androidx/core/view/KeyEventDispatcher",
-      "android/support/v4/view/LayoutInflaterCompat": "androidx/core/view/LayoutInflaterCompat",
-      "android/support/v4/view/LayoutInflaterFactory": "androidx/core/view/LayoutInflaterFactory",
-      "android/support/v4/view/MarginLayoutParamsCompat": "androidx/core/view/MarginLayoutParamsCompat",
-      "android/support/v4/view/MenuCompat": "androidx/core/view/MenuCompat",
-      "android/support/v4/view/MenuItemCompat": "androidx/core/view/MenuItemCompat",
-      "android/support/v4/view/MotionEventCompat": "androidx/core/view/MotionEventCompat",
-      "android/support/v4/view/NestedScrollingChild": "androidx/core/view/NestedScrollingChild",
-      "android/support/v4/view/NestedScrollingChild2": "androidx/core/view/NestedScrollingChild2",
-      "android/support/v4/view/NestedScrollingChildHelper": "androidx/core/view/NestedScrollingChildHelper",
-      "android/support/v4/view/NestedScrollingParent": "androidx/core/view/NestedScrollingParent",
-      "android/support/v4/view/NestedScrollingParent2": "androidx/core/view/NestedScrollingParent2",
-      "android/support/v4/view/NestedScrollingParentHelper": "androidx/core/view/NestedScrollingParentHelper",
-      "android/support/v4/view/OnApplyWindowInsetsListener": "androidx/core/view/OnApplyWindowInsetsListener",
-      "android/support/v4/view/PointerIconCompat": "androidx/core/view/PointerIconCompat",
-      "android/support/v4/view/ScaleGestureDetectorCompat": "androidx/core/view/ScaleGestureDetectorCompat",
-      "android/support/v4/view/ScrollingView": "androidx/core/view/ScrollingView",
-      "android/support/v4/view/TintableBackgroundView": "androidx/core/view/TintableBackgroundView",
-      "android/support/v4/view/VelocityTrackerCompat": "androidx/core/view/VelocityTrackerCompat",
-      "android/support/v4/view/ViewCompat": "androidx/core/view/ViewCompat",
-      "android/support/v4/view/ViewConfigurationCompat": "androidx/core/view/ViewConfigurationCompat",
-      "android/support/v4/view/ViewGroupCompat": "androidx/core/view/ViewGroupCompat",
-      "android/support/v4/view/ViewParentCompat": "androidx/core/view/ViewParentCompat",
-      "android/support/v4/view/ViewPropertyAnimatorCompat": "androidx/core/view/ViewPropertyAnimatorCompat",
-      "android/support/v4/view/ViewPropertyAnimatorListener": "androidx/core/view/ViewPropertyAnimatorListener",
-      "android/support/v4/view/ViewPropertyAnimatorListenerAdapter": "androidx/core/view/ViewPropertyAnimatorListenerAdapter",
-      "android/support/v4/view/ViewPropertyAnimatorUpdateListener": "androidx/core/view/ViewPropertyAnimatorUpdateListener",
-      "android/support/v4/view/WindowCompat": "androidx/core/view/WindowCompat",
-      "android/support/v4/view/WindowInsetsCompat": "androidx/core/view/WindowInsetsCompat",
-      "android/support/v4/view/accessibility/AccessibilityEventCompat": "androidx/core/view/accessibility/AccessibilityEventCompat",
-      "android/support/v4/view/accessibility/AccessibilityManagerCompat": "androidx/core/view/accessibility/AccessibilityManagerCompat",
-      "android/support/v4/view/accessibility/AccessibilityNodeInfoCompat": "androidx/core/view/accessibility/AccessibilityNodeInfoCompat",
-      "android/support/v4/view/accessibility/AccessibilityNodeProviderCompat": "androidx/core/view/accessibility/AccessibilityNodeProviderCompat",
-      "android/support/v4/view/accessibility/AccessibilityRecordCompat": "androidx/core/view/accessibility/AccessibilityRecordCompat",
-      "android/support/v4/view/accessibility/AccessibilityWindowInfoCompat": "androidx/core/view/accessibility/AccessibilityWindowInfoCompat",
-      "android/support/v4/view/animation/PathInterpolatorApi14": "androidx/core/view/animation/PathInterpolatorApi14",
-      "android/support/v4/view/animation/PathInterpolatorCompat": "androidx/core/view/animation/PathInterpolatorCompat",
-      "android/support/v4/widget/AutoScrollHelper": "androidx/core/widget/AutoScrollHelper",
-      "android/support/v4/widget/AutoSizeableTextView": "androidx/core/widget/AutoSizeableTextView",
-      "android/support/v4/widget/CompoundButtonCompat": "androidx/core/widget/CompoundButtonCompat",
-      "android/support/v4/widget/ContentLoadingProgressBar": "androidx/core/widget/ContentLoadingProgressBar",
-      "android/support/v4/widget/EdgeEffectCompat": "androidx/core/widget/EdgeEffectCompat",
-      "android/support/v4/widget/ImageViewCompat": "androidx/core/widget/ImageViewCompat",
-      "android/support/v4/widget/ListPopupWindowCompat": "androidx/core/widget/ListPopupWindowCompat",
-      "android/support/v4/widget/ListViewAutoScrollHelper": "androidx/core/widget/ListViewAutoScrollHelper",
-      "android/support/v4/widget/ListViewCompat": "androidx/core/widget/ListViewCompat",
-      "android/support/v4/widget/NestedScrollView": "androidx/core/widget/NestedScrollView",
-      "android/support/v4/widget/PopupMenuCompat": "androidx/core/widget/PopupMenuCompat",
-      "android/support/v4/widget/PopupWindowCompat": "androidx/core/widget/PopupWindowCompat",
-      "android/support/v4/widget/ScrollerCompat": "androidx/core/widget/ScrollerCompat",
-      "android/support/v4/widget/TextViewCompat": "androidx/core/widget/TextViewCompat",
-      "android/support/v4/widget/TintableCompoundButton": "androidx/core/widget/TintableCompoundButton",
-      "android/support/v4/widget/TintableImageSourceView": "androidx/core/widget/TintableImageSourceView",
       "android/support/v7/app/ActionBar": "androidx/appcompat/app/ActionBar",
       "android/support/v7/app/ActionBarDrawerToggle": "androidx/appcompat/app/ActionBarDrawerToggle",
       "android/support/v7/app/ActionBarDrawerToggleHoneycomb": "androidx/appcompat/app/ActionBarDrawerToggleHoneycomb",
@@ -4382,9 +4336,6 @@
         "android/support/v4/{any}",
         "androidx/{any}"
       ],
-      "android/support/v4/view/{any}": [
-        "androidx/core/view/{any}"
-      ],
       "android/support/v7/{any}": [
         "androidx/appcompat/{any}"
       ],
@@ -4398,9 +4349,6 @@
       "android/support/v7/internal/widget/ActionBarView${any}": [
         "androidx/appcompat/widget/AbsActionBarView${any}"
       ],
-      "android/support/v4/view/MenuItemCompat/*": [
-        "androidx/core/view/MenuItemCompat/*"
-      ],
       "Android{any}": [
         "Android{any}"
       ]
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 088ea33..b8da452 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -25,7 +25,7 @@
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.android.tools.lint.checks.ApiLookup
 import com.android.tools.lint.checks.ApiLookup.equivalentName
-import com.android.tools.lint.checks.ApiLookup.startsWithEquivalentPrefix
+import com.android.tools.lint.checks.DesugaredMethodLookup
 import com.android.tools.lint.checks.VersionChecks.Companion.codeNameToApi
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Context
@@ -415,48 +415,19 @@
                         return
                     }
                 }
-
-                // If it's a method we have source for, obviously it shouldn't be a
-                // violation. (This happens for example when compiling the support library.)
-                if (method !is PsiCompiledElement) {
-                    return
-                }
             }
 
-            // Desugar rewrites compare calls (see b/36390874)
-            if (name == "compare" &&
-                api == 19 &&
-                startsWithEquivalentPrefix(owner, "java/lang/") &&
-                desc.length == 4 &&
-                (
-                    desc == "(JJ)" ||
-                        desc == "(ZZ)" ||
-                        desc == "(BB)" ||
-                        desc == "(CC)" ||
-                        desc == "(II)" ||
-                        desc == "(SS)"
-                    )
-            ) {
-                if (context.project.isDesugaring(Desugaring.LONG_COMPARE)) {
-                    return
-                }
+            // Builtin R8 desugaring, such as rewriting compare calls (see b/36390874)
+            if (owner.startsWith("java.") &&
+                DesugaredMethodLookup.isDesugared(owner, name, desc)) {
+                return
             }
 
-            // Desugar rewrites Objects.requireNonNull calls (see b/32446315)
-            if (name == "requireNonNull" &&
-                api == 19 &&
-                owner == "java.util.Objects" &&
-                desc == "(Ljava.lang.Object;)"
-            ) {
-                if (context.project.isDesugaring(Desugaring.OBJECTS_REQUIRE_NON_NULL)) {
-                    return
-                }
-            }
-
-            if (name == "addSuppressed" &&
-                api == 19 &&
-                owner == "java.lang.Throwable" &&
-                desc == "(Ljava.lang.Throwable;)"
+            // These methods are not included in the R8 backported list so handle them manually
+            // the way R8 seems to
+            if (api == 19 && owner == "java.lang.Throwable" &&
+                (name == "addSuppressed" && desc == "(Ljava.lang.Throwable;)" ||
+                    name == "getSuppressed" && desc == "()")
             ) {
                 if (context.project.isDesugaring(Desugaring.TRY_WITH_RESOURCES)) {
                     return
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index ec2d6c4..260ecfd 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
 kotlin.code.style=official
 # Disable docs
 androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=7990267
+androidx.playground.snapshotBuildId=8005530
 androidx.playground.metalavaBuildId=7856580
 androidx.playground.dokkaBuildId=7472101
 androidx.studio.type=playground
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
index ac2eca84..dfd0add 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/DiagnosticsMessageCollector.kt
@@ -57,6 +57,10 @@
         message: String,
         location: CompilerMessageSourceLocation?
     ) {
+        if (message == KSP_ADDITIONAL_ERROR_MESSAGE) {
+            // ignore this as it will impact error counts.
+            return
+        }
         // Both KSP and KAPT reports null location but instead put the location into the message.
         // We parse it back here to recover the location.
         val (strippedMessage, rawLocation) = if (location == null) {
@@ -150,5 +154,10 @@
         private val KIND_REGEX = """^\w+: """.toRegex()
         // example: "[ksp] the real message"
         private val KSP_PREFIX_REGEX = """^\[ksp] """.toRegex()
+
+        // KSP always prints an additional error if any other error occurred.
+        // We drop that additional message to provide a more consistent error count with KAPT/javac.
+        private const val KSP_ADDITIONAL_ERROR_MESSAGE =
+            "Error occurred in KSP, check log for detail"
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
index 233cb41..b4104d3 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/TestKspRegistrar.kt
@@ -83,6 +83,7 @@
         }
         val logger = MessageCollectorBasedKSPLogger(
             messageCollector = messageCollector,
+            wrappedMessageCollector = messageCollector,
             allWarningsAsErrors = baseOptions.allWarningsAsErrors
         )
         val options = baseOptions.build()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index c7dd2e7..9807eef 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -25,6 +25,7 @@
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Variance
 
 /**
@@ -133,7 +134,8 @@
                     param.variance == Variance.CONTRAVARIANT ||
                         when (val decl = myType.declaration) {
                             is KSClassDeclaration -> {
-                                decl.isOpen() || decl.classKind == ClassKind.ENUM_CLASS
+                                decl.isOpen() || decl.classKind == ClassKind.ENUM_CLASS ||
+                                    decl.modifiers.contains(Modifier.SEALED)
                             }
                             else -> true
                         }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index a090664..43002a4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -132,7 +132,11 @@
         // turn them into string, provides better failure reports
         fun Map<String, List<TypeName>>.signatures(): List<Pair<String, String>> {
             return this.entries.map {
-                it.key to it.value.joinToString(" ")
+                it.key to it.value.joinToString(" ") {
+                    // javapoet doesn't always read enclosing class property from classpath
+                    // we don't care about it here.
+                    it.toString().replace('$', '.')
+                }
             }.sortedBy {
                 it.first
             }
@@ -155,6 +159,13 @@
                     VAL1,
                     VAL2;
                 }
+                sealed class GrandParentSealed {
+                    class Parent1: GrandParentSealed()
+                    sealed class Parent2: GrandParentSealed() {
+                        class Child1: Parent2()
+                        class Child2: Parent2()
+                    }
+                }
                 """.trimIndent()
         )
         return listOf(
@@ -239,6 +250,9 @@
                     numberList: List<Number>,
                     stringList: List<String>,
                     enumList: List<MyEnum>,
+                    sealedListGrandParent: List<GrandParentSealed>,
+                    sealedListParent: List<GrandParentSealed.Parent1>,
+                    sealedListChild: List<GrandParentSealed.Parent2.Child1>,
                     jvmWildcard: List<@JvmWildcard String>,
                     suppressJvmWildcard: List<@JvmSuppressWildcards Number>
                 ) {
@@ -248,6 +262,9 @@
                     var propWithOpenGeneric: List<Number> = TODO()
                     var propWithTypeArg: R = TODO()
                     var propWithTypeArgGeneric: List<R> = TODO()
+                    var propSealedListGrandParent: List<GrandParentSealed> = TODO()
+                    var propSealedListParent: List<GrandParentSealed.Parent1> = TODO()
+                    var propSealedListChild: List<GrandParentSealed.Parent2.Child1> = TODO()
                     @JvmSuppressWildcards
                     var propWithOpenTypeButSuppressAnnotation: Number = 3
                     fun list(list: List<*>): List<*> { TODO() }
@@ -255,6 +272,9 @@
                     fun listTypeArgNumber(list: List<Number>): List<Number> { TODO() }
                     fun listTypeArgString(list: List<String>): List<String> { TODO() }
                     fun listTypeArgEnum(list: List<MyEnum>): List<MyEnum> { TODO() }
+                    fun listSealedListGrandParent(list: List<GrandParentSealed>): List<GrandParentSealed> { TODO() }
+                    fun listSealedListParent(list: List<GrandParentSealed.Parent1>): List<GrandParentSealed.Parent1> { TODO() }
+                    fun listSealedListChild(list: List<GrandParentSealed.Parent2.Child1>): List<GrandParentSealed.Parent2.Child1> { TODO() }
                     fun explicitJvmWildcard(
                         list: List<@JvmWildcard String>
                     ): List<@JvmWildcard String> { TODO() }
@@ -271,6 +291,9 @@
                     fun suspendListTypeArgNumber(list: List<Number>): List<Number> { TODO() }
                     fun suspendListTypeArgString(list: List<String>): List<String> { TODO() }
                     fun suspendListTypeArgEnum(list: List<MyEnum>): List<MyEnum> { TODO() }
+                    fun suspendListSealedListGrandParent(list: List<GrandParentSealed>): List<GrandParentSealed> { TODO() }
+                    fun suspendListSealedListParent(list: List<GrandParentSealed.Parent1>): List<GrandParentSealed.Parent1> { TODO() }
+                    fun suspendListSealedListChild(list: List<GrandParentSealed.Parent2.Child1>): List<GrandParentSealed.Parent2.Child1> { TODO() }
                     fun suspendExplicitJvmWildcard(
                         list: List<@JvmWildcard String>
                     ): List<@JvmWildcard String> { TODO() }
diff --git a/settings.gradle b/settings.gradle
index afb72af..abab84a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -341,6 +341,9 @@
 includeProject(":compose:compiler:compiler-hosted", "compose/compiler/compiler-hosted", [BuildType.COMPOSE, BuildType.MAIN])
 includeProject(":compose:compiler:compiler-hosted:integration-tests", "compose/compiler/compiler-hosted/integration-tests", [BuildType.COMPOSE])
 includeProject(":compose:compiler:compiler-hosted:integration-tests:kotlin-compiler-repackaged", "compose/compiler/compiler-hosted/integration-tests/kotlin-compiler-repackaged", [BuildType.COMPOSE])
+includeProject(":compose:compiler:compiler-daemon", "compose/compiler/compiler-daemon", [BuildType.COMPOSE])
+includeProject(":compose:compiler:compiler-daemon:integration-tests", "compose/compiler/compiler-daemon/integration-tests", [BuildType.COMPOSE])
+
 if (isMultiplatformEnabled()) {
     includeProject(":compose:desktop", "compose/desktop", [BuildType.COMPOSE])
     includeProject(":compose:desktop:desktop", "compose/desktop/desktop", [BuildType.COMPOSE])
@@ -724,6 +727,7 @@
 includeProject(":wear:watchface:watchface-complications", "wear/watchface/watchface-complications", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-complications-permission-dialogs-sample", "wear/watchface/watchface-complications-permission-dialogs-sample", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-complications-data", "wear/watchface/watchface-complications-data", [BuildType.MAIN, BuildType.WEAR])
+includeProject(":wear:watchface:watchface-complications-data-core", "wear/watchface/watchface-complications-data-core", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-complications-data-source", "wear/watchface/watchface-complications-data-source", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-complications-data-source-ktx", "wear/watchface/watchface-complications-data-source-ktx", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-complications-data-source-samples", "wear/watchface/watchface-complications-data-source-samples", [BuildType.MAIN, BuildType.WEAR])
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 55b5e40..707c7ff 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -261,7 +261,7 @@
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, optional int anchorType, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -271,6 +271,17 @@
   public final class ScalingLazyColumnMeasureKt {
   }
 
+  @androidx.compose.runtime.Immutable public final inline class ScalingLazyListAnchorType {
+    ctor public ScalingLazyListAnchorType();
+  }
+
+  public static final class ScalingLazyListAnchorType.Companion {
+    method public int getItemCenter();
+    method public int getItemStart();
+    property public final int ItemCenter;
+    property public final int ItemStart;
+  }
+
   public interface ScalingLazyListItemInfo {
     method public float getAlpha();
     method public int getIndex();
@@ -295,12 +306,10 @@
   }
 
   public interface ScalingLazyListLayoutInfo {
-    method public int getCentralItemIndex();
     method public int getTotalItemsCount();
     method public int getViewportEndOffset();
     method public int getViewportStartOffset();
     method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
-    property public abstract int centralItemIndex;
     property public abstract int totalItemsCount;
     property public abstract int viewportEndOffset;
     property public abstract int viewportStartOffset;
@@ -313,11 +322,17 @@
   }
 
   @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
-    ctor public ScalingLazyListState();
+    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public float dispatchRawDelta(float delta);
+    method public int getCenterItemIndex();
+    method public int getCenterItemScrollOffset();
     method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final int centerItemIndex;
+    property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
     property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
     field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
@@ -329,7 +344,7 @@
   }
 
   public final class ScalingLazyListStateKt {
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState();
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
   }
 
   @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 9294509..8625dce 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -287,7 +287,7 @@
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, optional int anchorType, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -297,6 +297,17 @@
   public final class ScalingLazyColumnMeasureKt {
   }
 
+  @androidx.compose.runtime.Immutable public final inline class ScalingLazyListAnchorType {
+    ctor public ScalingLazyListAnchorType();
+  }
+
+  public static final class ScalingLazyListAnchorType.Companion {
+    method public int getItemCenter();
+    method public int getItemStart();
+    property public final int ItemCenter;
+    property public final int ItemStart;
+  }
+
   public interface ScalingLazyListItemInfo {
     method public float getAlpha();
     method public int getIndex();
@@ -321,12 +332,10 @@
   }
 
   public interface ScalingLazyListLayoutInfo {
-    method public int getCentralItemIndex();
     method public int getTotalItemsCount();
     method public int getViewportEndOffset();
     method public int getViewportStartOffset();
     method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
-    property public abstract int centralItemIndex;
     property public abstract int totalItemsCount;
     property public abstract int viewportEndOffset;
     property public abstract int viewportStartOffset;
@@ -339,11 +348,17 @@
   }
 
   @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
-    ctor public ScalingLazyListState();
+    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public float dispatchRawDelta(float delta);
+    method public int getCenterItemIndex();
+    method public int getCenterItemScrollOffset();
     method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final int centerItemIndex;
+    property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
     property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
     field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
@@ -355,7 +370,7 @@
   }
 
   public final class ScalingLazyListStateKt {
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState();
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
   }
 
   @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 55b5e40..707c7ff 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -261,7 +261,7 @@
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyListState state, optional int anchorType, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -271,6 +271,17 @@
   public final class ScalingLazyColumnMeasureKt {
   }
 
+  @androidx.compose.runtime.Immutable public final inline class ScalingLazyListAnchorType {
+    ctor public ScalingLazyListAnchorType();
+  }
+
+  public static final class ScalingLazyListAnchorType.Companion {
+    method public int getItemCenter();
+    method public int getItemStart();
+    property public final int ItemCenter;
+    property public final int ItemStart;
+  }
+
   public interface ScalingLazyListItemInfo {
     method public float getAlpha();
     method public int getIndex();
@@ -295,12 +306,10 @@
   }
 
   public interface ScalingLazyListLayoutInfo {
-    method public int getCentralItemIndex();
     method public int getTotalItemsCount();
     method public int getViewportEndOffset();
     method public int getViewportStartOffset();
     method public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
-    property public abstract int centralItemIndex;
     property public abstract int totalItemsCount;
     property public abstract int viewportEndOffset;
     property public abstract int viewportStartOffset;
@@ -313,11 +322,17 @@
   }
 
   @androidx.compose.runtime.Stable public final class ScalingLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
-    ctor public ScalingLazyListState();
+    ctor public ScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
+    method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public float dispatchRawDelta(float delta);
+    method public int getCenterItemIndex();
+    method public int getCenterItemScrollOffset();
     method public androidx.wear.compose.material.ScalingLazyListLayoutInfo getLayoutInfo();
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final int centerItemIndex;
+    property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
     property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
     field public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
@@ -329,7 +344,7 @@
   }
 
   public final class ScalingLazyListStateKt {
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState();
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyListState rememberScalingLazyListState(optional int initialCenterItemIndex, optional int initialCenterItemScrollOffset);
   }
 
   @kotlin.DslMarker public @interface ScalingLazyScopeMarker {
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
index 214ca1c..afa486d 100644
--- a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
@@ -19,14 +19,19 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.assertNoPendingChanges
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
 import androidx.compose.testutils.benchmark.benchmarkFirstCompose
-import androidx.compose.testutils.benchmark.benchmarkFirstDraw
-import androidx.compose.testutils.benchmark.benchmarkFirstLayout
 import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
 import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.compose.testutils.benchmark.recomposeUntilNoChangesPending
+import androidx.compose.testutils.doFramesUntilNoChangesPending
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -37,6 +42,7 @@
 import androidx.wear.compose.material.ScalingLazyColumn
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.rememberScalingLazyListState
+import org.junit.Assert
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -65,12 +71,12 @@
 
     @Test
     fun first_layout() {
-        benchmarkRule.benchmarkFirstLayout(scalingLazyColumnCaseFactory)
+        benchmarkRule.benchmarkFirstScalingLazyColumnLayout(scalingLazyColumnCaseFactory)
     }
 
     @Test
     fun first_draw() {
-        benchmarkRule.benchmarkFirstDraw(scalingLazyColumnCaseFactory)
+        benchmarkRule.benchmarkFirstScalingLazyColumnDraw(scalingLazyColumnCaseFactory)
     }
 
     @Test
@@ -111,4 +117,88 @@
             content()
         }
     }
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnLayout(
+    caseFactory: () -> LayeredComposeTestCase
+) {
+    runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+        measureRepeated {
+            runWithTimingDisabled {
+                doFramesUntilNoChangesPending()
+                // Add the content to benchmark
+                getTestCase().addMeasuredContent()
+                recomposeUntilNoChangesPending()
+                requestLayout()
+                measure()
+            }
+
+            layout()
+            recomposeUntilNoChangesPending()
+
+            runWithTimingDisabled {
+                assertNoPendingChanges()
+                disposeContent()
+            }
+        }
+    }
+}
+
+// TODO (b/210654937): Should be able to get rid of this workaround in the future once able to call
+// LaunchedEffect directly on underlying LazyColumn rather than via a 2-stage initialization via
+// onGloballyPositioned().
+fun ComposeBenchmarkRule.benchmarkFirstScalingLazyColumnDraw(
+    caseFactory: () -> LayeredComposeTestCase
+) {
+    runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
+        measureRepeated {
+            runWithTimingDisabled {
+                doFramesUntilNoChangesPending()
+                // Add the content to benchmark
+                getTestCase().addMeasuredContent()
+                recomposeUntilNoChangesPending()
+                requestLayout()
+                measure()
+                layout()
+                drawPrepare()
+            }
+
+            draw()
+            drawFinish()
+            recomposeUntilNoChangesPending()
+
+            runWithTimingDisabled {
+                assertNoPendingChanges()
+                disposeContent()
+            }
+        }
+    }
+}
+
+private class LayeredCaseAdapter(private val innerCase: LayeredComposeTestCase) : ComposeTestCase {
+
+    companion object {
+        fun of(caseFactory: () -> LayeredComposeTestCase): () -> LayeredCaseAdapter = {
+            LayeredCaseAdapter(caseFactory())
+        }
+    }
+
+    var isComposed by mutableStateOf(false)
+
+    @Composable
+    override fun Content() {
+        innerCase.ContentWrappers {
+            if (isComposed) {
+                innerCase.MeasuredContent()
+            }
+        }
+    }
+
+    fun addMeasuredContent() {
+        Assert.assertTrue(!isComposed)
+        isComposed = true
+    }
 }
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 89276dd..939306f 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -104,12 +104,13 @@
     }
 
     @Test
-    fun can_scroll_picker_down_on_start() {
+   fun can_scroll_picker_down_on_start() {
+        val numberOfOptions = 5
         lateinit var state: PickerState
         rule.setContent {
             WithTouchSlop(0f) {
                 Picker(
-                    5,
+                    numberOfOptions,
                     state = rememberPickerState().also { state = it },
                     modifier = Modifier.testTag(TEST_TAG)
                         .requiredSize(itemSizeDp * 3),
@@ -130,7 +131,9 @@
         }
 
         rule.waitForIdle()
-        assertThat(state.selectedOption).isEqualTo(initiallySelectedItem - 1)
+        val targetValue =
+            if (initiallySelectedItem == 0) numberOfOptions - 1 else initiallySelectedItem - 1
+        assertThat(state.selectedOption).isEqualTo(targetValue)
     }
 
     @Test
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index dd701f6..a8daba8 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -929,16 +929,12 @@
 
     private fun ScalingLazyListLayoutInfo.assertWhollyVisibleItems(
         firstItemIndex: Int,
-        firstItemNotVisible: Int = 0,
         lastItemIndex: Int,
-        lastItemNotVisible: Int = 0,
         viewPortHeight: Int
     ) {
         assertThat(visibleItemsInfo.first().index).isEqualTo(firstItemIndex)
-        assertThat(visibleItemsInfo.first().offset).isEqualTo(firstItemNotVisible)
         assertThat(visibleItemsInfo.last().index).isEqualTo(lastItemIndex)
-        assertThat(
-            viewPortHeight - (visibleItemsInfo.last().offset + visibleItemsInfo.last().size)
-        ).isEqualTo(lastItemNotVisible)
+        assertThat((viewPortHeight / 2f) >=
+            (visibleItemsInfo.last().offset + (visibleItemsInfo.last().size / 2)))
     }
 }
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
index 5b36b6a..5422045 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
@@ -136,7 +136,8 @@
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
-                    scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+                    scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                    contentPadding = PaddingValues(vertical = 100.dp)
                 ) {
                     items(5) {
                         Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
@@ -154,8 +155,8 @@
             )
         }
         rule.waitForIdle()
-        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
-        assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+        assertThat(state.centerItemIndex).isEqualTo(1)
     }
 
     @Test
@@ -164,14 +165,14 @@
         rule.setContent {
             WithTouchSlop(0f) {
                 ScalingLazyColumn(
-                    state = rememberScalingLazyListState().also { state = it },
+                    state = rememberScalingLazyListState(8).also { state = it },
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
-                        itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                        itemSizeDp * 4f + defaultItemSpacingDp * 3f
                     ),
                     scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
                     reverseLayout = true
                 ) {
-                    items(5) {
+                    items(15) {
                         Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
                     }
                 }
@@ -187,8 +188,9 @@
             )
         }
         rule.waitForIdle()
-        state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
-        assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+        state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
+        assertThat(state.centerItemIndex).isEqualTo(9)
+        assertThat(state.centerItemScrollOffset).isEqualTo(0)
     }
 
     @Composable
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index c5b3156..605c1a6 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -29,26 +29,23 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
 import kotlin.math.roundToInt
-import kotlin.properties.Delegates
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -166,13 +163,13 @@
     @Test
     fun itemStraddlingCenterLineDoesNotGetScaled() {
         lateinit var state: ScalingLazyListState
-        var viewPortHeight by Delegates.notNull<Int>()
+        val centerItemIndex = 2
         rule.setContent {
             ScalingLazyColumn(
-                state = rememberScalingLazyListState().also { state = it },
+                state = rememberScalingLazyListState(centerItemIndex).also { state = it },
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3
-                ).onSizeChanged { viewPortHeight = it.height },
+                ),
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -182,14 +179,12 @@
 
         rule.runOnIdle {
             // Get the middle item on the screen
-            val secondItem = state.layoutInfo.visibleItemsInfo[1]
-            // Confirm it's the second item in the list
-            assertThat(secondItem.index).isEqualTo(1)
-            // And that it is located either side of the center line
-            assertThat(secondItem.offset).isLessThan(viewPortHeight / 2)
-            assertThat(secondItem.offset + secondItem.size).isGreaterThan(viewPortHeight / 2)
+            val centerScreenItem =
+                state.layoutInfo.visibleItemsInfo.find { it.index == centerItemIndex }
+            // and confirm its offset is 0
+            assertThat(centerScreenItem!!.offset).isEqualTo(0)
             // And that it is not scaled
-            assertThat(secondItem.scale).isEqualTo(1.0f)
+            assertThat(centerScreenItem.scale).isEqualTo(1.0f)
         }
     }
 
@@ -219,13 +214,13 @@
     }
 
     @Test
-    fun visibleItemsAreCorrectNoScaling() {
+    fun visibleItemsAreCorrectCenterPivotNoOffset() {
         lateinit var state: ScalingLazyListState
         rule.setContent {
             ScalingLazyColumn(
-                state = rememberScalingLazyListState().also { state = it },
+                state = rememberScalingLazyListState(2).also { state = it },
                 modifier = Modifier.requiredSize(
-                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
                 ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
             ) {
@@ -236,8 +231,83 @@
         }
 
         rule.runOnIdle {
-            state.layoutInfo.assertVisibleItems(count = 4)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotWithOffset() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2, -5).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(-5)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotNoOffsetReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectCenterPivotWithOffsetReverseLayout() {
+        lateinit var state: ScalingLazyListState
+        rule.setContent {
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState(2, -5).also { state = it },
+                modifier = Modifier.requiredSize(
+                    itemSizeDp * 2f + defaultItemSpacingDp * 1f
+                ),
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                reverseLayout = true
+            ) {
+                items(5) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
+            assertThat(state.centerItemIndex).isEqualTo(2)
+            assertThat(state.centerItemScrollOffset).isEqualTo(-5)
         }
     }
 
@@ -246,15 +316,15 @@
         lateinit var state: ScalingLazyListState
         rule.setContent {
             ScalingLazyColumn(
-                state = rememberScalingLazyListState().also { state = it },
+                state = rememberScalingLazyListState(8).also { state = it },
                 modifier = Modifier.requiredSize(
-                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    itemSizeDp * 4f + defaultItemSpacingDp * 3f
                 ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
                 reverseLayout = true
             ) {
-                items(5) {
-                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+                items(15) {
+                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
                 }
             }
         }
@@ -262,13 +332,10 @@
         rule.waitForIdle()
 
         // Assert that items are being shown at the end of the parent as this is reverseLayout
-        rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
-        rule.onNodeWithTag(testTag = "Item:0")
-            .assertTopPositionInRootIsEqualTo(itemSizeDp * 2.5f + defaultItemSpacingDp * 2.5f)
+        rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
 
         rule.runOnIdle {
-            state.layoutInfo.assertVisibleItems(count = 4)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
         }
     }
 
@@ -281,10 +348,14 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                contentPadding = PaddingValues(vertical = 100.dp),
             ) {
                 items(5) {
-                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+                    Box(
+                        Modifier
+                            .requiredSize(itemSizeDp)
+                            .testTag("Item:$it"))
                 }
             }
         }
@@ -292,22 +363,24 @@
         rule.waitForIdle()
 
         rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
-        // Assert that the 0th item is displayed at the end of the parent for reversedLayout
-        rule.onNodeWithTag(testTag = "Item:0").assertTopPositionInRootIsEqualTo(0.dp)
 
+        val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
         rule.runOnIdle {
+            assertThat(state.centerItemIndex).isEqualTo(0)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
             runBlocking {
-                state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+                state.scrollBy(scrollAmount.toFloat())
             }
-            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 4)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(-scrollAmount)
         }
 
         rule.runOnIdle {
             runBlocking {
-                state.scrollBy(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+                state.scrollBy(-scrollAmount.toFloat())
             }
-            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+            state.layoutInfo.assertVisibleItems(count = 3)
             assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
         }
     }
@@ -317,40 +390,40 @@
         lateinit var state: ScalingLazyListState
         rule.setContent {
             ScalingLazyColumn(
-                state = rememberScalingLazyListState().also { state = it },
+                state = rememberScalingLazyListState(8).also { state = it },
                 modifier = Modifier.requiredSize(
-                    itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
+                    itemSizeDp * 4f + defaultItemSpacingDp * 3f
                 ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
                 reverseLayout = true
             ) {
-                items(5) {
-                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
+                items(15) {
+                    Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
                 }
             }
         }
 
         rule.waitForIdle()
 
-        rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
-        // Assert that the 0th item is displayed at the end of the parent for reversedLayout
-        rule.onNodeWithTag(testTag = "Item:0")
-            .assertTopPositionInRootIsEqualTo(itemSizeDp * 2.5f + defaultItemSpacingDp * 2.5f)
+        rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
 
+        val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
         rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
+            assertThat(state.centerItemIndex).isEqualTo(8)
+            assertThat(state.centerItemScrollOffset).isEqualTo(0)
+
             runBlocking {
-                state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+                state.scrollBy(scrollAmount.toFloat())
             }
-            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
         }
 
         rule.runOnIdle {
             runBlocking {
-                state.scrollBy(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+                state.scrollBy(-scrollAmount.toFloat())
             }
-            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
         }
     }
 
@@ -363,7 +436,8 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
+                scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                contentPadding = PaddingValues(vertical = 100.dp)
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -371,19 +445,21 @@
             }
         }
 
+        val scrollAmount = itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()
         rule.runOnIdle {
             runBlocking {
-                state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+                state.dispatchRawDelta(scrollAmount)
             }
-            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset)
+                .isEqualTo(-scrollAmount.roundToInt())
         }
 
         rule.runOnIdle {
             runBlocking {
-                state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
+                state.dispatchRawDelta(-scrollAmount)
             }
-            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
+            state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
             assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
         }
     }
@@ -405,13 +481,13 @@
                 }
             }
         }
-
+        val firstItemOffset = state.layoutInfo.visibleItemsInfo.first().offset
         rule.runOnIdle {
             runBlocking {
                 state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
             }
             state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
         }
 
         rule.runOnIdle {
@@ -419,7 +495,7 @@
                 state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
             }
             state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
-            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
+            assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
         }
     }
 
@@ -559,6 +635,44 @@
         }
     }
 
+    @Composable
+    fun ObservingCentralItemIndexFun(
+        state: ScalingLazyListState,
+        currentInfo: StableRef<Int?>
+    ) {
+        currentInfo.value = state.centerItemIndex
+    }
+
+    @Test
+    fun isCentralListItemIndexObservableWhenWeScroll() {
+        lateinit var state: ScalingLazyListState
+        var scope: CoroutineScope? = null
+        val currentInfo = StableRef<Int?>(null)
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            ScalingLazyColumn(
+                state = rememberScalingLazyListState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f)
+            ) {
+                items(6) {
+                    Box(Modifier.requiredSize(itemSizeDp))
+                }
+            }
+            ObservingCentralItemIndexFun(state, currentInfo)
+        }
+
+        scope!!.launch {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo.value = null
+            state.animateScrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo.value).isNotNull()
+            assertThat(currentInfo.value).isEqualTo(2)
+        }
+    }
+
     @Test
     fun visibleItemsAreObservableWhenResize() {
         lateinit var state: ScalingLazyListState
@@ -665,7 +779,7 @@
         }
     }
 
-    fun ScalingLazyListLayoutInfo.assertVisibleItems(
+    private fun ScalingLazyListLayoutInfo.assertVisibleItems(
         count: Int,
         startIndex: Int = 0,
         unscaledSize: Int = itemSizePx,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
index 4a28d8e..7ccfcf2 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
@@ -72,10 +72,11 @@
     }
 
     val repeatTarget = if (repeatItems) 100_000_000 / numberOfOptions else 1
-    if (repeatItems) {
+    // TODO: Remove this and pass into the initial position when creating the LazyListState.
+    if (repeatItems && state.scalingLazyListState.initialized.value) {
         LaunchedEffect(state, numberOfOptions) {
             // Scroll to the middle block.
-            state.scalingLazyListState.lazyListState.scrollToItem(
+            state.scalingLazyListState.scrollToItem(
                 numberOfOptions * (repeatTarget / 2),
                 0
             )
@@ -123,7 +124,7 @@
         get() = if (itemCount == 0) {
             0
         } else {
-            scalingLazyListState.layoutInfo.centralItemIndex % itemCount
+            scalingLazyListState.centerItemIndex % itemCount
         }
 
     /**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
index 1625ff2..a3254f6 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
@@ -421,20 +421,23 @@
     private fun decimalLastItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val lastItem = state.layoutInfo.visibleItemsInfo.last()
-        val lastItemVisibleSize = state.layoutInfo.viewportEndOffset - lastItem.offset
-        val decimalLastItemIndex = lastItem.index.toFloat() +
-            lastItemVisibleSize.toFloat() / lastItem.size.toFloat()
+        val lastItemConvertedOffset: Float = lastItem.offset - (lastItem.size / 2f) +
+            state.viewportHeightPx.value?.div(2f)!! + state.layoutInfo.viewportStartOffset
+        val lastItemVisibleSize = state.layoutInfo.viewportEndOffset - lastItemConvertedOffset
+        val decimalLastItemIndex = lastItem.index.toFloat() + lastItemVisibleSize /
+            lastItem.size.toFloat()
         return decimalLastItemIndex
     }
 
     private fun decimalFirstItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val firstItem = state.layoutInfo.visibleItemsInfo.first()
-        val firstItemOffset = firstItem.offset - state.layoutInfo.viewportStartOffset
+        val firstItemConvertedOffset: Float = firstItem.offset - (firstItem.size / 2f) +
+            state.viewportHeightPx.value?.div(2f)!! + state.layoutInfo.viewportStartOffset
         val decimalFirstItemIndex =
-            if (firstItemOffset < 0)
+            if (firstItemConvertedOffset < 0)
                 firstItem.index.toFloat() +
-                    abs(firstItemOffset.toFloat()) / firstItem.size.toFloat()
+                    abs(firstItemConvertedOffset) / firstItem.size.toFloat()
             else firstItem.index.toFloat()
         return decimalFirstItemIndex
     }
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index 8378453..2baa159 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -29,13 +29,19 @@
 import androidx.compose.foundation.lazy.LazyListScope
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
@@ -161,6 +167,47 @@
     itemContent(it, items[it])
 }
 
+@Suppress("INLINE_CLASS_DEPRECATED")
+@Immutable
+public inline class ScalingLazyListAnchorType internal constructor(internal val type: Int) {
+
+    companion object {
+        /**
+         * Place the center of the item on (or as close to) the center line of the viewport
+         */
+        val ItemCenter = ScalingLazyListAnchorType(0)
+
+        /**
+         * Place the start (edge) of the item on, or as close to as possible, the center line of the
+         * viewport. For normal layout this will be the top edge of the item, for reverseLayout it
+         * will be the bottom edge.
+         */
+        val ItemStart = ScalingLazyListAnchorType(1)
+    }
+
+    override fun toString(): String {
+        return when (this) {
+            ItemStart -> "ScalingLazyListAnchorType.ItemStart"
+            else -> "ScalingLazyListAnchorType.ItemCenter"
+        }
+    }
+}
+
+internal fun convertToCenterOffset(
+    anchorType: ScalingLazyListAnchorType,
+    itemScrollOffset: Int,
+    viewPortSizeInPx: Int,
+    beforeContentPaddingInPx: Int,
+    itemSizeInPx: Int
+): Int {
+    if (anchorType == ScalingLazyListAnchorType.ItemStart) {
+        return itemScrollOffset - (viewPortSizeInPx / 2) + beforeContentPaddingInPx
+    } else {
+        return itemScrollOffset + (itemSizeInPx / 2) -
+            (viewPortSizeInPx / 2) + beforeContentPaddingInPx
+    }
+}
+
 /**
  * A scrolling scaling/fisheye list component that forms a key part of the Wear Material Design
  * language. Provides scaling and transparency effects to the content items.
@@ -204,24 +251,19 @@
     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
     contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
     state: ScalingLazyListState = rememberScalingLazyListState(),
+    anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
     content: ScalingLazyListScope.() -> Unit
 ) {
-    require(scalingParams.minElementHeight <= scalingParams.maxElementHeight) {
-        "minElementHeight must be less than or equal to maxElementHeight"
-    }
-    require(scalingParams.minTransitionArea <= scalingParams.maxTransitionArea) {
-        "minTransitionArea must be less than or equal to maxTransitionArea"
-    }
-    require(scalingParams.minElementHeight != scalingParams.maxElementHeight ||
-        scalingParams.minTransitionArea == scalingParams.maxTransitionArea) {
-        "when minElementHeight and maxElementHeight are equal, " +
-            "so should be minTransitionArea and maxTransitionArea"
-    }
+    var initialized by remember { mutableStateOf(false) }
     BoxWithConstraints(modifier = modifier) {
         val density = LocalDensity.current
         val layoutDirection = LocalLayoutDirection.current
         val extraPaddingInPixels = scalingParams.resolveViewportVerticalOffset(constraints)
         val extraPadding = with(density) { extraPaddingInPixels.toDp() }
+        val beforeContentPaddingInPx = with(density) {
+            if (reverseLayout) contentPadding.calculateBottomPadding().roundToPx()
+            else contentPadding.calculateTopPadding().roundToPx()
+        }
         val itemScope = with(density) {
             ScalingLazyListItemScopeImpl(
                 density = density,
@@ -239,11 +281,13 @@
         }
         // Set up transient state
         state.scalingParams.value = scalingParams
-        state.extraPaddingInPixels.value = extraPaddingInPixels
+        state.extraPaddingPx.value = extraPaddingInPixels
+        state.beforeContentPaddingPx.value = beforeContentPaddingInPx
         state.viewportHeightPx.value = constraints.maxHeight
         state.gapBetweenItemsPx.value = with(density) {
             verticalArrangement.spacing.roundToPx()
         }
+        state.anchorType.value = anchorType
         state.reverseLayout.value = reverseLayout
 
         val combinedPaddingValues = CombinedPaddingValues(
@@ -251,10 +295,13 @@
             extraPadding = extraPadding
         )
         LazyColumn(
-            Modifier
+            modifier = Modifier
                 .fillMaxSize()
                 .clipToBounds()
-                .verticalNegativePadding(extraPadding),
+                .verticalNegativePadding(extraPadding)
+                .onGloballyPositioned {
+                    initialized = true
+                },
             horizontalAlignment = horizontalAlignment,
             contentPadding = combinedPaddingValues,
             reverseLayout = reverseLayout,
@@ -268,6 +315,11 @@
             )
             scope.content()
         }
+        if (initialized) {
+            LaunchedEffect(state) {
+                state.scrollToInitialItem()
+            }
+        }
     }
 }
 
@@ -449,15 +501,15 @@
 ) {
     Box(
         modifier = Modifier.graphicsLayer {
-            val items = state.layoutInfo.visibleItemsInfo
             val reverseLayout = state.reverseLayout.value!!
+            val items = state.layoutInfo.visibleItemsInfo
             val currentItem = items.find { it.index == index }
             if (currentItem != null) {
                 alpha = currentItem.alpha
                 scaleX = currentItem.scale
                 scaleY = currentItem.scale
                 val offsetAdjust = (currentItem.offset - currentItem.unadjustedOffset).toFloat()
-                translationY = if (reverseLayout) - offsetAdjust else offsetAdjust
+                translationY = if (reverseLayout) -offsetAdjust else offsetAdjust
                 transformOrigin = TransformOrigin(
                     pivotFractionX = 0.5f,
                     pivotFractionY = if (reverseLayout) 1.0f else 0.0f
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index 3466825..764c34a 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -188,13 +188,11 @@
 
     init {
         check(
-            minElementHeight <= maxElementHeight,
-            { "minElementHeight must be less than or equal to maxElementHeight" }
-        )
+            minElementHeight <= maxElementHeight
+        ) { "minElementHeight must be less than or equal to maxElementHeight" }
         check(
-            minTransitionArea <= maxTransitionArea,
-            { "minTransitionArea must be less than or equal to maxTransitionArea" }
-        )
+            minTransitionArea <= maxTransitionArea
+        ) { "minTransitionArea must be less than or equal to maxTransitionArea" }
     }
 
     override fun resolveViewportVerticalOffset(viewportConstraints: Constraints): Int {
@@ -288,20 +286,17 @@
     // TODO(b/202164558) - double check the height calculations with UX
     val heightAsFractionOfHalfViewPort = itemHeightPx / viewPortEdgeToCenterPx
     if (itemEdgeAsFractionOfHalfViewport > 0.0f && itemEdgeAsFractionOfHalfViewport < 1.0f) {
-        val scalingLineAsFractionOfViewPort =
-            if (scalingParams.minTransitionArea == scalingParams.maxTransitionArea) {
-                scalingParams.minTransitionArea
-            } else {
-                // Work out the scaling line based on size, this is a value between 0.0..1.0
-                val sizeRatio: Float = (
-                    (heightAsFractionOfHalfViewPort - scalingParams.minElementHeight) /
-                        (scalingParams.maxElementHeight - scalingParams.minElementHeight)
-                    ).coerceIn(0f, 1f)
+        // Work out the scaling line based on size, this is a value between 0.0..1.0
+        val sizeRatio: Float =
+            (
+                (heightAsFractionOfHalfViewPort - scalingParams.minElementHeight) /
+                    (scalingParams.maxElementHeight - scalingParams.minElementHeight)
+                ).coerceIn(0f, 1f)
 
-                scalingParams.minTransitionArea +
-                    (scalingParams.maxTransitionArea - scalingParams.minTransitionArea) *
-                    sizeRatio
-            }
+        val scalingLineAsFractionOfViewPort =
+            scalingParams.minTransitionArea +
+                (scalingParams.maxTransitionArea - scalingParams.minTransitionArea) *
+                sizeRatio
 
         if (itemEdgeAsFractionOfHalfViewport < scalingLineAsFractionOfViewPort) {
             // We are scaling
@@ -313,12 +308,12 @@
 
             scaleToApply =
                 scalingParams.edgeScale +
-                (1.0f - scalingParams.edgeScale) *
-                (1.0f - fractionOfDiffToApplyInterpolated)
+                    (1.0f - scalingParams.edgeScale) *
+                    (1.0f - fractionOfDiffToApplyInterpolated)
             alphaToApply =
                 scalingParams.edgeAlpha +
-                (1.0f - scalingParams.edgeAlpha) *
-                (1.0f - fractionOfDiffToApplyInterpolated)
+                    (1.0f - scalingParams.edgeAlpha) *
+                    (1.0f - fractionOfDiffToApplyInterpolated)
         }
     } else {
         scaleToApply = scalingParams.edgeScale
@@ -340,14 +335,22 @@
  * viewport in order to correctly calculate the scaling to apply.
  * @param viewportHeightPx the height of the viewport in pixels
  * @param scalingParams the scaling params to use for determining the scaled size of the item
+ * @param beforeContentPaddingPx the number of pixels of padding before the first item
+ * @param anchorType the type of pivot to use for the center item when calculating position and
+ * offset
+ * @param initialized a flag to determine whether the ScalingLazyColumn is initialized or not, if
+ * not then set the item to be transparent.
  */
-internal fun createItemInfo(
+internal fun calculateItemInfo(
     itemStart: Int,
     item: LazyListItemInfo,
     verticalAdjustment: Int,
     viewportHeightPx: Int,
     scalingParams: ScalingParams,
-): ScalingLazyListItemInfo {
+    beforeContentPaddingPx: Int,
+    anchorType: ScalingLazyListAnchorType,
+    initialized: Boolean
+): ItemInfoAndOffsetDelta {
     val adjustedItemStart = itemStart - verticalAdjustment
     val adjustedItemEnd = itemStart + item.size - verticalAdjustment
 
@@ -364,14 +367,32 @@
         itemStart + item.size - scaledHeight
     }
 
-    return DefaultScalingLazyListItemInfo(
-        index = item.index,
-        key = item.key,
-        unadjustedOffset = item.offset,
-        offset = scaledItemTop,
-        size = scaledHeight,
-        scale = scaleAndAlpha.scale,
-        alpha = scaleAndAlpha.alpha
+    val offset = convertToCenterOffset(
+        anchorType = anchorType,
+        itemScrollOffset = scaledItemTop,
+        viewPortSizeInPx = viewportHeightPx,
+        beforeContentPaddingInPx = beforeContentPaddingPx,
+        itemSizeInPx = item.size
+    )
+    val offsetDelta = scaledItemTop - offset
+    val unadjustedOffset = convertToCenterOffset(
+        anchorType = anchorType,
+        itemScrollOffset = item.offset,
+        viewPortSizeInPx = viewportHeightPx,
+        beforeContentPaddingInPx = beforeContentPaddingPx,
+        itemSizeInPx = item.size
+    )
+    return ItemInfoAndOffsetDelta(
+        offsetDelta,
+        DefaultScalingLazyListItemInfo(
+            index = item.index,
+            key = item.key,
+            unadjustedOffset = unadjustedOffset,
+            offset = offset,
+            size = scaledHeight,
+            scale = scaleAndAlpha.scale,
+            alpha = if (initialized) scaleAndAlpha.alpha else 0f
+        )
     )
 }
 
@@ -380,7 +401,8 @@
     override val viewportStartOffset: Int,
     override val viewportEndOffset: Int,
     override val totalItemsCount: Int,
-    override val centralItemIndex: Int
+    val centerItemIndex: Int,
+    val centerItemScrollOffset: Int
 ) : ScalingLazyListLayoutInfo
 
 internal class DefaultScalingLazyListItemInfo(
@@ -391,7 +413,13 @@
     override val size: Int,
     override val scale: Float,
     override val alpha: Float
-) : ScalingLazyListItemInfo
+) : ScalingLazyListItemInfo {
+    override fun toString(): String {
+        return "DefaultScalingLazyListItemInfo(index=$index, key=$key, " +
+            "unadjustedOffset=$unadjustedOffset, offset=$offset, size=$size, " +
+            "scale=$scale, alpha=$alpha)"
+    }
+}
 
 @Immutable
 internal data class ScaleAndAlpha(
@@ -403,3 +431,13 @@
         internal val noScaling = ScaleAndAlpha(1.0f, 1.0f)
     }
 }
+
+@Immutable
+internal data class ItemInfoAndOffsetDelta(
+    val offsetDelta: Int,
+    val itemInfo: ScalingLazyListItemInfo
+) {
+    fun offsetAdjusted(): Int {
+        return itemInfo.offset + offsetDelta
+    }
+}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
index 18e07cf..83ef1a8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfo.kt
@@ -48,10 +48,4 @@
      * The total count of items passed to [ScalingLazyColumn].
      */
     val totalItemsCount: Int
-
-    /**
-     * The index of the item on to the center of the view, if there are two items around the center
-     * line, the second one (higher index) is used. It's -1 if the list is empty.
-     */
-    val centralItemIndex: Int
 }
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index f8fe40a..03a4fd9 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
@@ -29,121 +30,179 @@
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
+import kotlin.math.roundToInt
 
 /**
  * Creates a [ScalingLazyListState] that is remembered across compositions.
+ *
+ * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex]
+ * @param initialCenterItemScrollOffset the initial value for
+ * [ScalingLazyListState.centerItemScrollOffset] in pixels
  */
 @Composable
-public fun rememberScalingLazyListState(): ScalingLazyListState {
+public fun rememberScalingLazyListState(
+    initialCenterItemIndex: Int = 0,
+    initialCenterItemScrollOffset: Int = 0
+): ScalingLazyListState {
     return rememberSaveable(saver = ScalingLazyListState.Saver) {
-        ScalingLazyListState()
+        ScalingLazyListState(
+            initialCenterItemIndex,
+            initialCenterItemScrollOffset
+        )
     }
 }
 
 /**
  * A state object that can be hoisted to control and observe scrolling.
- * TODO (b/193792848): Add scrolling and snap support.
  *
  * In most cases, this will be created via [rememberScalingLazyListState].
+ *
+ * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex]
+ * @param initialCenterItemScrollOffset the initial value for
+ * [ScalingLazyListState.centerItemScrollOffset]
  */
+// TODO (b/193792848): Add snap support.
 @Stable
-public class ScalingLazyListState : ScrollableState {
+class ScalingLazyListState constructor(
+    private val initialCenterItemIndex: Int = 0,
+    private val initialCenterItemScrollOffset: Int = 0
+) : ScrollableState {
+
     internal var lazyListState: LazyListState = LazyListState(0, 0)
-    internal val extraPaddingInPixels = mutableStateOf<Int?>(null)
+    internal val extraPaddingPx = mutableStateOf<Int?>(null)
+    internal val beforeContentPaddingPx = mutableStateOf<Int?>(null)
     internal val scalingParams = mutableStateOf<ScalingParams?>(null)
     internal val gapBetweenItemsPx = mutableStateOf<Int?>(null)
     internal val viewportHeightPx = mutableStateOf<Int?>(null)
     internal val reverseLayout = mutableStateOf<Boolean?>(null)
+    internal val anchorType = mutableStateOf<ScalingLazyListAnchorType?>(null)
+    internal val initialized = mutableStateOf<Boolean>(false)
+
+    /**
+     * The index of the item positioned closest to the viewport center
+     */
+    public val centerItemIndex: Int
+        get() = (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.centerItemIndex ?: 0
+
+    /**
+     * The offset of the item closest to the viewport center. Depending on the [ScalingLazyListAnchorType] of the
+     * [ScalingLazyColumn] the offset will be relative to either items Edge or Center.
+     */
+    public val centerItemScrollOffset: Int
+        get() = (layoutInfo as? DefaultScalingLazyListLayoutInfo)?.centerItemScrollOffset ?: 0
 
     /**
      * The object of [ScalingLazyListLayoutInfo] calculated during the last layout pass. For
      * example, you can use it to calculate what items are currently visible.
      */
     public val layoutInfo: ScalingLazyListLayoutInfo by derivedStateOf {
-        if (extraPaddingInPixels.value == null || scalingParams.value == null ||
+        if (extraPaddingPx.value == null || scalingParams.value == null ||
             gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
-            reverseLayout.value == null
+            anchorType.value == null || reverseLayout.value == null ||
+            beforeContentPaddingPx.value == null
         ) {
             EmptyScalingLazyListLayoutInfo
         } else {
             val visibleItemsInfo = mutableListOf<ScalingLazyListItemInfo>()
-            var centralItemIndex = -1
+            var newCenterItemIndex = -1
+            var newCenterItemScrollOffset = 0
 
             if (lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty()) {
                 val verticalAdjustment =
-                    lazyListState.layoutInfo.viewportStartOffset + extraPaddingInPixels.value!!
+                    lazyListState.layoutInfo.viewportStartOffset + extraPaddingPx.value!!
 
                 // Find the item in the middle of the viewport
                 val centralItem =
                     findItemNearestCenter(viewportHeightPx.value!!, verticalAdjustment)!!
 
                 // Place the center item
-                val centerItemInfo = createItemInfo(
+                val centerItemInfoAndOffsetDelta = calculateItemInfo(
                     centralItem.offset,
                     centralItem,
                     verticalAdjustment,
                     viewportHeightPx.value!!,
                     scalingParams.value!!,
+                    beforeContentPaddingPx.value!!,
+                    anchorType.value!!,
+                    initialized.value
                 )
                 visibleItemsInfo.add(
-                    centerItemInfo
+                    centerItemInfoAndOffsetDelta.itemInfo
                 )
+
+                newCenterItemIndex = centralItem.index
+                newCenterItemScrollOffset = - centerItemInfoAndOffsetDelta.itemInfo.offset
+
                 // Go Up
-                centralItemIndex = centralItem.index
-                var nextItemBottomNoPadding = centerItemInfo.offset - gapBetweenItemsPx.value!!
+                var nextItemBottomNoPadding =
+                    centerItemInfoAndOffsetDelta.offsetAdjusted() - gapBetweenItemsPx.value!!
                 val minIndex =
                     lazyListState.layoutInfo.visibleItemsInfo.minOf { it.index }
-                (centralItemIndex - 1 downTo minIndex).forEach { ix ->
+                (newCenterItemIndex - 1 downTo minIndex).forEach { ix ->
                     val currentItem =
-                        lazyListState.layoutInfo.visibleItemsInfo.find { it.index == ix }!!
-                    val itemInfo = createItemInfo(
+                        lazyListState.layoutInfo.findItemInfoWithIndex(ix)!!
+                    val itemInfoAndOffset = calculateItemInfo(
                         nextItemBottomNoPadding - currentItem.size,
                         currentItem,
                         verticalAdjustment,
                         viewportHeightPx.value!!,
                         scalingParams.value!!,
+                        beforeContentPaddingPx.value!!,
+                        anchorType.value!!,
+                        initialized.value
                     )
                     // If the item is visible in the viewport insert it at the start of the
                     // list
-                    if ((itemInfo.offset + itemInfo.size) > verticalAdjustment) {
+                    if (
+                        (itemInfoAndOffset.offsetAdjusted() + itemInfoAndOffset.itemInfo.size) >
+                        verticalAdjustment) {
                         // Insert the item info at the front of the list
-                        visibleItemsInfo.add(0, itemInfo)
+                        visibleItemsInfo.add(0, itemInfoAndOffset.itemInfo)
                     }
-                    nextItemBottomNoPadding = itemInfo.offset - gapBetweenItemsPx.value!!
+                    nextItemBottomNoPadding =
+                        itemInfoAndOffset.offsetAdjusted() - gapBetweenItemsPx.value!!
                 }
                 // Go Down
                 var nextItemTopNoPadding =
-                    centerItemInfo.offset + centerItemInfo.size +
+                    centerItemInfoAndOffsetDelta.offsetAdjusted() +
+                        centerItemInfoAndOffsetDelta.itemInfo.size +
                         gapBetweenItemsPx.value!!
                 val maxIndex =
                     lazyListState.layoutInfo.visibleItemsInfo.maxOf { it.index }
-                (centralItemIndex + 1..maxIndex).forEach { ix ->
+                (newCenterItemIndex + 1..maxIndex).forEach { ix ->
                     val currentItem =
-                        lazyListState.layoutInfo.visibleItemsInfo.find { it.index == ix }!!
-                    val itemInfo = createItemInfo(
+                        lazyListState.layoutInfo.findItemInfoWithIndex(ix)!!
+                    val itemInfoAndOffset = calculateItemInfo(
                         nextItemTopNoPadding,
                         currentItem,
                         verticalAdjustment,
                         viewportHeightPx.value!!,
                         scalingParams.value!!,
+                        beforeContentPaddingPx.value!!,
+                        anchorType.value!!,
+                        initialized.value
                     )
                     // If the item is visible in the viewport insert it at the end of the
                     // list
-                    if ((itemInfo.offset - verticalAdjustment) < viewportHeightPx.value!!) {
-                        visibleItemsInfo.add(itemInfo)
+                    if ((itemInfoAndOffset.offsetAdjusted() - verticalAdjustment) <
+                        viewportHeightPx.value!!) {
+                        visibleItemsInfo.add(itemInfoAndOffset.itemInfo)
                     }
                     nextItemTopNoPadding =
-                        itemInfo.offset + itemInfo.size + gapBetweenItemsPx.value!!
+                        itemInfoAndOffset.offsetAdjusted() + itemInfoAndOffset.itemInfo.size +
+                            gapBetweenItemsPx.value!!
                 }
             }
+
             DefaultScalingLazyListLayoutInfo(
                 visibleItemsInfo = visibleItemsInfo,
                 totalItemsCount = lazyListState.layoutInfo.totalItemsCount,
                 viewportStartOffset = lazyListState.layoutInfo.viewportStartOffset +
-                    extraPaddingInPixels.value!!,
+                    extraPaddingPx.value!!,
                 viewportEndOffset = lazyListState.layoutInfo.viewportEndOffset -
-                    extraPaddingInPixels.value!!,
-                centralItemIndex = centralItemIndex
+                    extraPaddingPx.value!!,
+                centerItemIndex = if (initialized.value) newCenterItemIndex else 0,
+                centerItemScrollOffset = if (initialized.value) newCenterItemScrollOffset else 0
             )
         }
     }
@@ -173,16 +232,12 @@
         val Saver = listSaver<ScalingLazyListState, Int>(
             save = {
                 listOf(
-                    it.lazyListState.firstVisibleItemIndex,
-                    it.lazyListState.firstVisibleItemScrollOffset,
+                    it.centerItemIndex,
+                    it.centerItemScrollOffset,
                 )
             },
             restore = {
-                val scalingLazyColumnState = ScalingLazyListState()
-                scalingLazyColumnState.lazyListState = LazyListState(
-                    firstVisibleItemIndex = it[0],
-                    firstVisibleItemScrollOffset = it[1],
-                )
+                val scalingLazyColumnState = ScalingLazyListState(it[0], it[1])
                 scalingLazyColumnState
             }
         )
@@ -203,6 +258,109 @@
     ) {
         lazyListState.scroll(scrollPriority = scrollPriority, block = block)
     }
+
+    /**
+     * Instantly brings the item at [index] to the center of the viewport and positions it based on
+     * the [anchorType] and applies the [scrollOffset] pixels.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll. Note that
+     * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+     * scroll the item further upward (taking it partly offscreen).
+     */
+    public suspend fun scrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        /*@IntRange(from = 0)*/
+        scrollOffset: Int = 0
+    ) {
+        val offsetToCenterOfViewport =
+            beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
+        if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+            val offset = offsetToCenterOfViewport + scrollOffset
+            return lazyListState.scrollToItem(index, offset)
+        } else {
+            var item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+            if (item == null) {
+                // Scroll the item into the middle of the viewport so that we know it is visible
+                lazyListState.scrollToItem(
+                    index,
+                    offsetToCenterOfViewport
+                )
+                // Now we know that the item is visible find it and fine tune our position
+                item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+            }
+            if (item != null) {
+                val offset = offsetToCenterOfViewport + (item.size / 2) + scrollOffset
+                return lazyListState.scrollToItem(index, offset)
+            }
+        }
+        return
+    }
+
+    internal suspend fun scrollToInitialItem() {
+        if (!initialized.value) {
+            initialized.value = true
+            scrollToItem(initialCenterItemIndex, initialCenterItemScrollOffset)
+        }
+        return
+    }
+
+    /**
+     * Animate (smooth scroll) the given item at [index] to the center of the viewport and position
+     * it based on the [anchorType] and applies the [scrollOffset] pixels.
+     *
+     * @param index the index to which to scroll. Must be non-negative.
+     * @param scrollOffset the offset that the item should end up after the scroll (same as
+     * [scrollToItem]) - note that positive offset refers to forward scroll, so in a
+     * top-to-bottom list, positive offset will scroll the item further upward (taking it partly
+     * offscreen)
+     */
+    public suspend fun animateScrollToItem(
+        /*@IntRange(from = 0)*/
+        index: Int,
+        /*@IntRange(from = 0)*/
+        scrollOffset: Int = 0
+    ) {
+        val offsetToCenterOfViewport =
+            beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
+        if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
+            val offset = offsetToCenterOfViewport + scrollOffset
+            return lazyListState.animateScrollToItem(index, offset)
+        } else {
+            var item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+            var sizeEstimate = 0
+            if (item == null) {
+                // Guess the size of the item so that we can try and position it correctly
+                sizeEstimate = lazyListState.layoutInfo.averageItemSize()
+                // Scroll the item towards the middle of the viewport so that we know it is visible
+                lazyListState.animateScrollToItem(
+                    index,
+                    offsetToCenterOfViewport + (sizeEstimate / 2) + scrollOffset
+                )
+                // Now we know that the item is visible find it and fine tune our position
+                item = lazyListState.layoutInfo.findItemInfoWithIndex(index)
+            }
+            // Determine if a second adjustment is needed
+            if (item != null && item.size != sizeEstimate) {
+                val offset = offsetToCenterOfViewport + (item.size / 2) + scrollOffset
+                return lazyListState.animateScrollToItem(index, offset)
+            }
+        }
+        return
+    }
+}
+
+private fun LazyListLayoutInfo.findItemInfoWithIndex(index: Int): LazyListItemInfo? {
+    return this.visibleItemsInfo.find { it.index == index }
+}
+
+private fun LazyListLayoutInfo.averageItemSize(): Int {
+    var totalSize = 0
+    visibleItemsInfo.forEach { totalSize += it.size }
+    return if (visibleItemsInfo.isNotEmpty())
+        (totalSize.toFloat() / visibleItemsInfo.size).roundToInt()
+    else 0
 }
 
 private object EmptyScalingLazyListLayoutInfo : ScalingLazyListLayoutInfo {
@@ -210,5 +368,4 @@
     override val viewportStartOffset = 0
     override val viewportEndOffset = 0
     override val totalItemsCount = 0
-    override val centralItemIndex = -1
 }
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index cfc5ea3..b9cf22d 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -38,11 +38,11 @@
 
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-    androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(project(":wear:compose:compose-material"))
     androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
     androidTestImplementation(libs.truth)
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
 
     samples(project(":wear:compose:compose-navigation-samples"))
 }
diff --git a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
index 64f4b73..4169ba6 100644
--- a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
+++ b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
@@ -15,8 +15,12 @@
  */
 package androidx.wear.compose.navigation
 
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.foundation.layout.Column
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.saveable.rememberSaveable
@@ -35,6 +39,7 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeRight
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.navigation.NavHostController
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.CompactChip
@@ -88,6 +93,36 @@
     }
 
     @Test
+    fun navigates_back_to_previous_level_with_back_button() {
+        val lifecycleOwner = TestLifecycleOwner()
+        val onBackPressedDispatcher = OnBackPressedDispatcher()
+        val dispatcherOwner = object : OnBackPressedDispatcherOwner {
+            override fun getLifecycle() = lifecycleOwner.lifecycle
+            override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
+        }
+        lateinit var navController: NavHostController
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides dispatcherOwner) {
+                navController = rememberSwipeDismissableNavController()
+                SwipeDismissWithNavigation(navController)
+            }
+        }
+        // Move to next destination.
+        rule.onNodeWithText(START).performClick()
+
+        // Now trigger the back button
+        rule.runOnIdle {
+            onBackPressedDispatcher.onBackPressed()
+        }
+        rule.waitForIdle()
+
+        // Should now display "start".
+        rule.onNodeWithText(START).assertExists()
+        assertThat(navController.currentDestination?.route).isEqualTo(START)
+    }
+
+    @Test
     fun hides_previous_level_when_not_swiping() {
         rule.setContentWithTheme {
             SwipeDismissWithNavigation()
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index 9d0cf94..ac2df15 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.compose.navigation
 
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
@@ -129,10 +130,23 @@
         "SwipeDismissableNavHost requires a ViewModelStoreOwner to be provided " +
             "via LocalViewModelStoreOwner"
     }
+    val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+    val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher
 
     // Setup the navController with proper owners
     navController.setLifecycleOwner(lifecycleOwner)
     navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
+    if (onBackPressedDispatcher != null) {
+        navController.setOnBackPressedDispatcher(onBackPressedDispatcher)
+    }
+    // Ensure that the NavController only receives back events while
+    // the NavHost is in composition
+    DisposableEffect(navController) {
+        navController.enableOnBackPressed(true)
+        onDispose {
+            navController.enableOnBackPressed(false)
+        }
+    }
 
     // Then set the graph
     navController.graph = graph
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt
index 55b0d70..b555540 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/CheckAccessibilityAvailable.kt
@@ -16,7 +16,6 @@
 
 package androidx.wear.tiles.checkers
 
-import androidx.annotation.RestrictTo
 import androidx.wear.tiles.LayoutElementBuilders.Arc
 import androidx.wear.tiles.LayoutElementBuilders.ArcAdapter
 import androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement
@@ -41,11 +40,8 @@
  *
  * At least one element on each tile should have a machine-readable content description
  * associated with it, which can be read out using screen readers.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class CheckAccessibilityAvailable : TimelineEntryChecker {
+internal class CheckAccessibilityAvailable : TimelineEntryChecker {
     override val name: String
         get() = "CheckAccessibilityAvailable"
 
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt
index 072230d..8665a76 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/checkers/TimelineChecker.kt
@@ -17,28 +17,21 @@
 package androidx.wear.tiles.checkers
 
 import android.util.Log
-import androidx.annotation.RestrictTo
 import androidx.wear.tiles.TimelineBuilders.Timeline
 import androidx.wear.tiles.TimelineBuilders.TimelineEntry
 import kotlin.jvm.Throws
 
 /**
  * Exception thrown when a TimelineEntryChecker fails.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class CheckerException(message: String) : Exception(message)
+internal class CheckerException(message: String) : Exception(message)
 
 /**
  * Checker for a Tile's TimelineEntries. Instances of this interface should check for a certain
  * condition on the given [TimelineEntry], and throw an instance of [CheckerException] if there
  * is a problem with that [TimelineEntry].
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface TimelineEntryChecker {
+internal interface TimelineEntryChecker {
     /** The name of this TimelineEntryChecker. This will be printed in any error output. */
     val name: String
 
@@ -56,11 +49,8 @@
  * given [Timeline], and if any fail, log an error to logcat.
  *
  * @param entryCheckers The list of checkers to use. Defaults to all built in checks.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class TimelineChecker(
+internal class TimelineChecker(
     private val entryCheckers: List<TimelineEntryChecker> = listOf(CheckAccessibilityAvailable()),
 ) {
     companion object {
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index ba67d7b..56d34c3 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -923,13 +923,19 @@
         interactiveInstance.complicationSlotsState
 
         // Add some additional ContentDescriptionLabels
+        val pendingIntent1 = PendingIntent.getActivity(context, 0, Intent("One"),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+        val pendingIntent2 = PendingIntent.getActivity(context, 0, Intent("Two"),
+            PendingIntent.FLAG_IMMUTABLE
+        )
         wallpaperService.watchFace.renderer.additionalContentDescriptionLabels = listOf(
             Pair(
                 0,
                 ContentDescriptionLabel(
                     PlainComplicationText.Builder("Before").build(),
                     Rect(10, 10, 20, 20),
-                    null
+                    pendingIntent1
                 )
             ),
             Pair(
@@ -937,7 +943,7 @@
                 ContentDescriptionLabel(
                     PlainComplicationText.Builder("After").build(),
                     Rect(30, 30, 40, 40),
-                    null
+                    pendingIntent2
                 )
             )
         )
@@ -960,6 +966,7 @@
         assertThat(
             contentDescriptionLabels[1].getTextAt(context.resources, Instant.EPOCH)
         ).isEqualTo("Before")
+        assertThat(contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent1)
 
         // Left complication.
         assertThat(contentDescriptionLabels[2].bounds).isEqualTo(Rect(80, 160, 160, 240))
@@ -978,6 +985,7 @@
         assertThat(
             contentDescriptionLabels[4].getTextAt(context.resources, Instant.EPOCH)
         ).isEqualTo("After")
+        assertThat(contentDescriptionLabels[4].tapAction).isEqualTo(pendingIntent2)
     }
 
     @SuppressLint("NewApi") // renderWatchFaceToBitmap
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
index 8ee7af8..9a2c34f 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
@@ -17,7 +17,6 @@
 package androidx.wear.watchface.client
 
 import android.os.RemoteException
-import androidx.annotation.RestrictTo
 import androidx.wear.watchface.editor.IEditorObserver
 import androidx.wear.watchface.editor.IEditorService
 import androidx.wear.watchface.editor.data.EditorStateWireFormat
@@ -50,9 +49,7 @@
     public fun onEditorStateChanged(editorState: EditorState)
 }
 
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class EditorServiceClientImpl(
+internal class EditorServiceClientImpl(
     private val iEditorService: IEditorService
 ) : EditorServiceClient {
     private val lock = Any()
diff --git a/wear/watchface/watchface-complications-data-core/api/current.txt b/wear/watchface/watchface-complications-data-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/watchface/watchface-complications-data-core/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/watchface/watchface-complications-data-core/api/res-current.txt b/wear/watchface/watchface-complications-data-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/res-current.txt
diff --git a/wear/watchface/watchface-complications-data-core/api/restricted_current.txt b/wear/watchface/watchface-complications-data-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/watchface/watchface-complications-data-core/build.gradle b/wear/watchface/watchface-complications-data-core/build.gradle
new file mode 100644
index 0000000..e3fc7b0
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api("androidx.annotation:annotation:1.1.0")
+    implementation("androidx.core:core:1.1.0")
+    implementation("androidx.preference:preference:1.1.0")
+    implementation("androidx.annotation:annotation:1.2.0")
+
+    constraints {
+        implementation(project(":wear:watchface:watchface-complications-data"))
+    }
+
+    testImplementation(libs.kotlinStdlib)
+    testImplementation(libs.kotlinCoroutinesAndroid)
+    testImplementation(libs.testCore)
+    testImplementation(libs.testRunner)
+    testImplementation(libs.testRules)
+    testImplementation(libs.robolectric)
+    testImplementation(libs.mockitoCore)
+    testImplementation(libs.truth)
+    testImplementation("junit:junit:4.13")
+}
+
+android {
+    buildFeatures {
+        aidl = true
+    }
+    defaultConfig {
+        minSdkVersion 26
+    }
+    buildTypes.all {
+        consumerProguardFiles "proguard-rules.pro"
+    }
+
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+}
+
+androidx {
+    name = "Android Wear Complications Data Internal"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.WEAR_WATCHFACE
+    inceptionYear = "2021"
+    description = "Android Wear Complications Data Internal"
+}
diff --git a/wear/watchface/watchface-complications-data/lint-baseline.xml b/wear/watchface/watchface-complications-data-core/lint-baseline.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/lint-baseline.xml
rename to wear/watchface/watchface-complications-data-core/lint-baseline.xml
diff --git a/wear/watchface/watchface-complications-data/proguard-rules.pro b/wear/watchface/watchface-complications-data-core/proguard-rules.pro
similarity index 100%
rename from wear/watchface/watchface-complications-data/proguard-rules.pro
rename to wear/watchface/watchface-complications-data-core/proguard-rules.pro
diff --git a/wear/watchface/watchface-complications-data-core/src/main/AndroidManifest.xml b/wear/watchface/watchface-complications-data-core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5b067ac
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.wear.watchface.complications.data.internal">
+    <application>
+        <uses-library android:name="com.google.android.wearable" android:required="false" />
+    </application>
+</manifest>
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationData.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/ComplicationProviderInfo.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationManager.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IPreviewComplicationDataCallback.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl b/wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl
rename to wear/watchface/watchface-complications-data-core/src/main/aidl/android/support/wearable/complications/IProviderInfoService.aidl
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/CharSequenceSerializableHelper.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationData.java
similarity index 97%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationData.java
index 3a29289..226d0f1 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -40,7 +40,6 @@
 import java.io.Serializable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -604,54 +603,54 @@
     }
 
     /**
-     * For timeline entries. Returns the {@link Instant} at which this timeline entry becomes
+     * For timeline entries. Returns the epoch second at which this timeline entry becomes
      * valid or `null` if it's not set.
      */
     @Nullable
-    public Instant getTimelineStartInstant() {
+    public Long getTimelineStartEpochSecond() {
         long expiresAt = mFields.getLong(FIELD_TIMELINE_START_TIME, -1);
         if (expiresAt == -1) {
             return null;
         } else {
-            return Instant.ofEpochSecond(expiresAt);
+            return expiresAt;
         }
     }
 
     /**
-     * For timeline entries. Sets the {@link Instant} at which this timeline entry becomes invalid
+     * For timeline entries. Sets the epoch second at which this timeline entry becomes invalid
      * or clears the field if instant is `null`.
      */
-    public void setTimelineStartInstant(@Nullable Instant instant) {
-        if (instant == null) {
+    public void setTimelineStartEpochSecond(@Nullable Long epochSecond) {
+        if (epochSecond == null) {
             mFields.remove(FIELD_TIMELINE_START_TIME);
         } else {
-            mFields.putLong(FIELD_TIMELINE_START_TIME, instant.getEpochSecond());
+            mFields.putLong(FIELD_TIMELINE_START_TIME, epochSecond);
         }
     }
 
     /**
-     * For timeline entries. Returns the {@link Instant} at which this timeline entry becomes
-     * invalid or `null` if it's not set.
+     * For timeline entries. Returns the epoch second at which this timeline entry becomes invalid
+     * or `null` if it's not set.
      */
     @Nullable
-    public Instant getTimelineEndInstant() {
+    public Long getTimelineEndEpochSecond() {
         long expiresAt = mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
         if (expiresAt == -1) {
             return null;
         } else {
-            return Instant.ofEpochSecond(expiresAt);
+            return expiresAt;
         }
     }
 
     /**
-     * For timeline entries. Sets the {@link Instant} at which this timeline entry becomes invalid,
+     * For timeline entries. Sets the epoch second at which this timeline entry becomes invalid,
      * or clears the field if instant is `null`.
      */
-    public void setTimelineEndInstant(@Nullable Instant instant) {
-        if (instant == null) {
+    public void setTimelineEndEpochSecond(@Nullable Long epochSecond) {
+        if (epochSecond == null) {
             mFields.remove(FIELD_TIMELINE_END_TIME);
         } else {
-            mFields.putLong(FIELD_TIMELINE_END_TIME, instant.getEpochSecond());
+            mFields.putLong(FIELD_TIMELINE_END_TIME, epochSecond);
         }
     }
 
@@ -674,9 +673,12 @@
         if (timelineEntries == null) {
             mFields.remove(FIELD_TIMELINE_ENTRIES);
         } else {
-            mFields.putParcelableArray(
-                    FIELD_TIMELINE_ENTRIES,
-                    timelineEntries.stream().map(e-> e.mFields).toArray(Parcelable[]::new));
+            Parcelable[] array = new Parcelable[timelineEntries.size()];
+            int index = 0;
+            for (ComplicationData timelineEntry : timelineEntries) {
+                array[index++] = timelineEntry.mFields;
+            }
+            mFields.putParcelableArray(FIELD_TIMELINE_ENTRIES, array);
         }
     }
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationProviderInfo.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationText.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationText.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/ComplicationTextTemplate.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDependentText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDependentText.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDependentText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDependentText.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDifferenceText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
index 60d5669..ba7122b 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
+++ b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeDifferenceText.java
@@ -23,7 +23,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.wear.watchface.complications.data.R;
+import androidx.wear.watchface.complications.data.internal.R;
 
 import java.io.InvalidObjectException;
 import java.io.ObjectInputStream;
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeFormatText.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeFormatText.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/TimeFormatText.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/TimeFormatText.java
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/package-info.java b/wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/package-info.java
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/package-info.java
rename to wear/watchface/watchface-complications-data-core/src/main/java/android/support/wearable/complications/package-info.java
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-af/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-af/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-af/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-af/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-am/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-am/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-am/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-am/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ar/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ar/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ar/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ar/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-as/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-as/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-as/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-as/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-az/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-az/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-az/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-az/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-b+sr+Latn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-b+sr+Latn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-b+sr+Latn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-b+sr+Latn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-be/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-be/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-be/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-be/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-bg/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-bg/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-bg/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-bg/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-bn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-bn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-bn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-bn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-bs/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-bs/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-bs/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-bs/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ca/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ca/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ca/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ca/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-cs/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-cs/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-cs/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-cs/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-da/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-da/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-da/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-da/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-de/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-de/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-de/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-de/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-el/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-el/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-el/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-el/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rAU/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rAU/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rAU/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rAU/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rCA/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rCA/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rCA/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rCA/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rGB/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rGB/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rGB/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rGB/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rIN/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rIN/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rIN/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rIN/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-en-rXC/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-en-rXC/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-en-rXC/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-en-rXC/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-es-rUS/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-es-rUS/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-es-rUS/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-es-rUS/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-es/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-es/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-es/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-es/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-et/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-et/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-et/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-et/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-eu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-eu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-eu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-eu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fa/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fa/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fa/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fa/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fi/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fi/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fi/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fi/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fr-rCA/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fr-rCA/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fr-rCA/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fr-rCA/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-fr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-fr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-fr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-fr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-gl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-gl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-gl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-gl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-gu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-gu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-gu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-gu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hi/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hi/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hi/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hi/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-hy/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-hy/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-hy/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-hy/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-in/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-in/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-in/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-in/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-is/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-is/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-is/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-is/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-it/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-it/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-it/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-it/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-iw/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-iw/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-iw/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-iw/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ja/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ja/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ja/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ja/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ka/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ka/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ka/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ka/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-kk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-kk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-kk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-kk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-km/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-km/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-km/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-km/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-kn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-kn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-kn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-kn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ko/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ko/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ko/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ko/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ky/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ky/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ky/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ky/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-lo/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-lo/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-lo/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-lo/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-lt/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-lt/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-lt/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-lt/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-lv/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-lv/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-lv/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-lv/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-mk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-mk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-mk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-mk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ml/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ml/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ml/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ml/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-mn/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-mn/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-mn/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-mn/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-mr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-mr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-mr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-mr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ms/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ms/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ms/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ms/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-my/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-my/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-my/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-my/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-nb/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-nb/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-nb/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-nb/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ne/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ne/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ne/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ne/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-nl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-nl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-nl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-nl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-or/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-or/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-or/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-or/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pa/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pa/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pa/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pa/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pt-rBR/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rBR/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pt-rBR/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rBR/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pt-rPT/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rPT/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pt-rPT/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pt-rPT/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-pt/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-pt/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-pt/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-pt/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ro/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ro/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ro/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ro/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ru/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ru/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ru/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ru/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-si/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-si/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-si/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-si/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sq/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sq/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sq/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sq/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sv/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sv/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sv/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sv/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-sw/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-sw/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-sw/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-sw/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ta/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ta/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ta/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ta/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-te/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-te/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-te/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-te/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-th/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-th/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-th/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-th/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-tl/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-tl/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-tl/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-tl/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-tr/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-tr/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-tr/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-tr/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-uk/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-uk/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-uk/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-uk/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-ur/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-ur/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-ur/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-ur/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-uz/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-uz/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-uz/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-uz/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-vi/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-vi/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-vi/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-vi/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zh-rCN/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rCN/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zh-rCN/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rCN/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zh-rHK/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rHK/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zh-rHK/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rHK/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zh-rTW/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rTW/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zh-rTW/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zh-rTW/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values-zu/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values-zu/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values-zu/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values-zu/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/main/res/values/complication_strings.xml b/wear/watchface/watchface-complications-data-core/src/main/res/values/complication_strings.xml
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/main/res/values/complication_strings.xml
rename to wear/watchface/watchface-complications-data-core/src/main/res/values/complication_strings.xml
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index dce3876..85a9927 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -23,7 +23,6 @@
 import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Assert.assertThrows
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
similarity index 98%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
index 1ceb670..9853608 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTemplateTest.kt
@@ -20,7 +20,6 @@
 import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
index 2a85fef..4e662a5 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
@@ -21,7 +21,6 @@
 import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/Parcelables.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/Parcelables.kt
similarity index 100%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/Parcelables.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/Parcelables.kt
diff --git a/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/SharedRobolectricTestRunner.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/SharedRobolectricTestRunner.kt
new file mode 100644
index 0000000..17a5ffd
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/SharedRobolectricTestRunner.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wearable.complications
+
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/** A test runner for all tests within this package. */
+public class SharedRobolectricTestRunner(private val testClass: Class<*>) :
+    RobolectricTestRunner(testClass) {
+
+    override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration =
+        InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)).apply {
+            doNotInstrumentPackage("androidx.wear")
+            doNotInstrumentPackage("android.support.wearable")
+        }.build()
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
index ef1d1a0..fae2cc6 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeDifferenceTextTest.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
similarity index 99%
rename from wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
rename to wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
index 16ec776..cc83239 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
+++ b/wear/watchface/watchface-complications-data-core/src/test/java/android/support/wearable/complications/TimeFormatTextTest.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Test
diff --git a/wear/watchface/watchface-complications-data-core/src/test/resources/robolectric.properties b/wear/watchface/watchface-complications-data-core/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..ce87047
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-core/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
+# sdk for now. Remove when no longer necessary.
+sdk=29
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index 0c324d5..a246b2c 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -27,7 +27,6 @@
 import android.support.wearable.complications.ComplicationProviderInfo
 import android.support.wearable.complications.IComplicationManager
 import android.support.wearable.complications.IComplicationProvider
-import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationType
@@ -573,12 +572,9 @@
          * complication data source chooser interface. If set to "true", users will not be able
          * to select this complication data source. The complication data source may still be
          * specified as a default complication data source by watch faces.
-         *
-         * @hide
          */
         // TODO(b/192233205): Migrate value to androidx.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        public const val METADATA_KEY_HIDDEN: String =
+        internal const val METADATA_KEY_HIDDEN: String =
             "android.support.wearable.complications.HIDDEN"
 
         /**
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
index 37a96a6..268dfc0 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
@@ -130,8 +130,8 @@
     internal fun asWireComplicationData(): WireComplicationData {
         val wireTimelineEntries = timelineEntries.map { timelineEntry ->
             timelineEntry.complicationData.asWireComplicationData().apply {
-                timelineStartInstant = timelineEntry.validity.start
-                timelineEndInstant = timelineEntry.validity.end
+                timelineStartEpochSecond = timelineEntry.validity.start.epochSecond
+                timelineEndEpochSecond = timelineEntry.validity.end.epochSecond
             }
         }
         return defaultComplicationData.asWireComplicationData().apply {
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java
index 9e3e54a..aefa3c6 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceServiceTest.java
@@ -331,22 +331,14 @@
                 data.getValue().getTimelineEntries();
         assertThat(timeLineEntries).isNotNull();
         assertThat(timeLineEntries.size()).isEqualTo(2);
-        assertThat(timeLineEntries.get(0).getTimelineStartInstant()).isEqualTo(
-                Instant.ofEpochSecond(1000)
-        );
-        assertThat(timeLineEntries.get(0).getTimelineEndInstant()).isEqualTo(
-                Instant.ofEpochSecond(4000)
-        );
+        assertThat(timeLineEntries.get(0).getTimelineStartEpochSecond()).isEqualTo(1000);
+        assertThat(timeLineEntries.get(0).getTimelineEndEpochSecond()).isEqualTo(4000);
         assertThat(timeLineEntries.get(0).getLongText().getTextAt(null, 0)).isEqualTo(
                 "A"
         );
 
-        assertThat(timeLineEntries.get(1).getTimelineStartInstant()).isEqualTo(
-                Instant.ofEpochSecond(6000)
-        );
-        assertThat(timeLineEntries.get(1).getTimelineEndInstant()).isEqualTo(
-                Instant.ofEpochSecond(8000)
-        );
+        assertThat(timeLineEntries.get(1).getTimelineStartEpochSecond()).isEqualTo(6000);
+        assertThat(timeLineEntries.get(1).getTimelineEndEpochSecond()).isEqualTo(8000);
         assertThat(timeLineEntries.get(1).getLongText().getTextAt(null, 0)).isEqualTo(
                 "B"
         );
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index d2e255d..1266b0f 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -14,10 +14,7 @@
  * limitations under the License.
  */
 
-import androidx.build.RunApiTasks
-
 import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 
 plugins {
@@ -30,6 +27,7 @@
     api("androidx.annotation:annotation:1.1.0")
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesAndroid)
+    api(project(":wear:watchface:watchface-complications-data-core"))
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.preference:preference:1.1.0")
     implementation("androidx.annotation:annotation:1.2.0")
@@ -43,15 +41,9 @@
 }
 
 android {
-    buildFeatures {
-        aidl = true
-    }
     defaultConfig {
         minSdkVersion 26
     }
-    buildTypes.all {
-        consumerProguardFiles "proguard-rules.pro"
-    }
 
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 4f6ed04..ab7ecb0 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -57,7 +57,7 @@
          *
          * @hide
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @JvmStatic
         public fun fromWireType(wireType: Int): ComplicationType =
             when (wireType) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 8cbc383..b1828b1 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -688,8 +688,8 @@
         timelineEntries?.let {
             for (entry in it) {
                 val wireEntry = entry.asWireComplicationData()
-                val start = wireEntry.timelineStartInstant?.epochSecond
-                val end = wireEntry.timelineEndInstant?.epochSecond
+                val start = wireEntry.timelineStartEpochSecond
+                val end = wireEntry.timelineEndEpochSecond
                 if (start != null && end != null && time >= start && time < end) {
                     val duration = end - start
                     if (duration < previousShortest) {
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 6e57671..04d6be7 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -2620,8 +2620,8 @@
         val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
             .setShortText(ComplicationText.plainText("B"))
             .build()
-        b.timelineStartInstant = Instant.ofEpochSecond(1000)
-        b.timelineEndInstant = Instant.MAX
+        b.timelineStartEpochSecond = 1000
+        b.timelineEndEpochSecond = Long.MAX_VALUE
         a.setTimelineEntryCollection(listOf(b))
 
         // Set the ComplicationData.
@@ -3546,6 +3546,10 @@
 
     @Test
     public fun additionalContentDescriptionLabelsSetBeforeWatchFaceInitComplete() {
+        val pendingIntent = PendingIntent.getActivity(context, 0, Intent("Example"),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+
         testWatchFaceService = TestWatchFaceService(
             WatchFaceType.ANALOG,
             emptyList(),
@@ -3564,7 +3568,7 @@
                         ContentDescriptionLabel(
                             PlainComplicationText.Builder("Example").build(),
                             Rect(10, 10, 20, 20),
-                            null
+                            pendingIntent
                         )
                     )
                 )
@@ -3624,6 +3628,8 @@
                 0
             )
         ).isEqualTo("Example")
+
+        assertThat(engineWrapper.contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent)
     }
 
     @Test
@@ -4170,14 +4176,14 @@
         val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
             .setShortText(ComplicationText.plainText("B"))
             .build()
-        b.timelineStartInstant = Instant.ofEpochSecond(1000)
-        b.timelineEndInstant = Instant.ofEpochSecond(4000)
+        b.timelineStartEpochSecond = 1000
+        b.timelineEndEpochSecond = 4000
 
         val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
             .setShortText(ComplicationText.plainText("C"))
             .build()
-        c.timelineStartInstant = Instant.ofEpochSecond(2000)
-        c.timelineEndInstant = Instant.ofEpochSecond(3000)
+        c.timelineStartEpochSecond = 2000
+        c.timelineEndEpochSecond = 3000
 
         a.setTimelineEntryCollection(listOf(b, c))
 
@@ -4222,14 +4228,14 @@
         val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
             .setShortText(ComplicationText.plainText("B"))
             .build()
-        b.setTimelineStartInstant(Instant.ofEpochSecond(1000))
-        b.setTimelineEndInstant(Instant.ofEpochSecond(2000))
+        b.timelineStartEpochSecond = 1000
+        b.timelineEndEpochSecond = 2000
 
         val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
             .setShortText(ComplicationText.plainText("C"))
             .build()
-        c.setTimelineStartInstant(Instant.ofEpochSecond(3000))
-        c.setTimelineEndInstant(Instant.ofEpochSecond(4000))
+        c.timelineStartEpochSecond = 3000
+        c.timelineEndEpochSecond = 4000
 
         a.setTimelineEntryCollection(listOf(b, c))
 
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
index 9dc5761..47c4577 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
@@ -22,7 +22,6 @@
 import android.os.Parcel
 import android.os.ResultReceiver
 import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.concurrent.futures.CallbackToFutureAdapter
 import androidx.wear.remote.interactions.RemoteInteractionsUtil.isCurrentDeviceAWatch
@@ -283,13 +282,10 @@
 
     /**
      * Result code passed to [ResultReceiver.send] for the status of remote intent.
-     *
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef(RESULT_OK, RESULT_FAILED)
     @Retention(AnnotationRetention.SOURCE)
-    public annotation class SendResult
+    internal annotation class SendResult
 
     public class RemoteIntentException(message: String) : Exception(message)
 
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index d9ebc25..48d2490 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -76,7 +76,6 @@
 import androidx.work.worker.EchoingWorker;
 import androidx.work.worker.ExceptionWorker;
 import androidx.work.worker.FailureWorker;
-import androidx.work.worker.InfiniteTestWorker;
 import androidx.work.worker.InterruptionAwareWorker;
 import androidx.work.worker.LatchWorker;
 import androidx.work.worker.RetryWorker;
@@ -100,6 +99,7 @@
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
@@ -116,6 +116,7 @@
     private ProgressUpdater mMockProgressUpdater;
     private ForegroundUpdater mMockForegroundUpdater;
     private Executor mSynchronousExecutor = new SynchronousExecutor();
+    private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
 
     @Before
     public void setUp() {
@@ -135,6 +136,12 @@
 
     @After
     public void tearDown() {
+        mExecutorService.shutdown();
+        try {
+            assertThat(mExecutorService.awaitTermination(3, TimeUnit.SECONDS), is(true));
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
         mDatabase.close();
     }
 
@@ -328,7 +335,7 @@
         insertWork(work);
         WorkerWrapper wrapper = createBuilder(work.getStringId()).build();
         FutureListener listener = createAndAddFutureListener(wrapper);
-        Executors.newSingleThreadExecutor().submit(wrapper);
+        mExecutorService.submit(wrapper);
         Thread.sleep(2000L); // Async wait duration.
         assertThat(mWorkSpecDao.getState(work.getStringId()), is(RUNNING));
         Thread.sleep(SleepTestWorker.SLEEP_DURATION);
@@ -974,7 +981,7 @@
                         .withSchedulers(Collections.singletonList(mMockScheduler))
                         .build();
         FutureListener listener = createAndAddFutureListener(workerWrapper);
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
         workerWrapper.interrupt();
         Thread.sleep(1000L);
         assertThat(listener.mResult, is(true));
@@ -992,7 +999,7 @@
                         .withSchedulers(Collections.singletonList(mMockScheduler))
                         .build();
         FutureListener listener = createAndAddFutureListener(workerWrapper);
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
         Thread.sleep(200);
         workerWrapper.interrupt();
         Thread.sleep(1000L);
@@ -1007,6 +1014,7 @@
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(LatchWorker.class).build();
         insertWork(work);
 
+        ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
         LatchWorker latchWorker =
                 (LatchWorker) mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
                         mContext.getApplicationContext(),
@@ -1017,7 +1025,7 @@
                                 work.getTags(),
                                 new WorkerParameters.RuntimeExtras(),
                                 1,
-                                Executors.newSingleThreadExecutor(),
+                                backgroundExecutor,
                                 mWorkTaskExecutor,
                                 mConfiguration.getWorkerFactory(),
                                 mMockProgressUpdater,
@@ -1029,7 +1037,7 @@
                         .withWorker(latchWorker)
                         .build();
         FutureListener listener = createAndAddFutureListener(workerWrapper);
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
 
         Thread.sleep(1000L);
 
@@ -1042,6 +1050,8 @@
 
         assertThat(listener.mResult, is(notNullValue()));
         verify(mMockScheduler, times(1)).cancel(work.getStringId());
+        backgroundExecutor.shutdown();
+        assertThat(backgroundExecutor.awaitTermination(3, TimeUnit.SECONDS), is(true));
     }
 
     @Test
@@ -1073,7 +1083,7 @@
                         .withSchedulers(Collections.singletonList(mMockScheduler))
                         .withWorker(worker)
                         .build();
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
         workerWrapper.interrupt();
         assertThat(worker.isStopped(), is(true));
         assertThat(mWorkSpecDao.getState(work.getStringId()), is(ENQUEUED));
@@ -1108,7 +1118,7 @@
                         .withSchedulers(Collections.singletonList(mMockScheduler))
                         .withWorker(worker)
                         .build();
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
         workerWrapper.interrupt();
         assertThat(worker.isStopped(), is(true));
         assertThat(mWorkSpecDao.getState(work.getStringId()), is(ENQUEUED));
@@ -1139,7 +1149,7 @@
                         .withSchedulers(Collections.singletonList(mMockScheduler))
                         .build();
         FutureListener listener = createAndAddFutureListener(workerWrapper);
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
         mWorkSpecDao.delete(work.getStringId());
         Thread.sleep(6000L);
         assertThat(listener.mResult, is(false));
@@ -1148,7 +1158,7 @@
     @Test
     @LargeTest
     public void testWorker_getsRunAttemptCount() throws InterruptedException {
-        OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(InfiniteTestWorker.class)
+        OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(LatchWorker.class)
                 .setInitialRunAttemptCount(10)
                 .build();
         insertWork(work);
@@ -1158,9 +1168,10 @@
                         .withSchedulers(Collections.singletonList(mMockScheduler))
                         .build();
 
-        Executors.newSingleThreadExecutor().submit(workerWrapper);
+        mExecutorService.submit(workerWrapper);
         Thread.sleep(1000L);
         assertThat(workerWrapper.mWorker.getRunAttemptCount(), is(10));
+        ((LatchWorker) workerWrapper.mWorker).mLatch.countDown();
     }
 
     @Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index 1ded228..d872142 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -49,10 +49,9 @@
 import androidx.work.impl.Processor;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.WorkManagerImpl;
-import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
+import androidx.work.impl.constraints.NetworkState;
 import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
+import androidx.work.impl.constraints.trackers.ConstraintTracker;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.model.WorkSpecDao;
@@ -93,67 +92,74 @@
     private static final int TEST_TIMEOUT = 6;
 
     private Context mContext;
-    private Scheduler mScheduler;
     private WorkManagerImpl mWorkManager;
-    private Configuration mConfiguration;
-    private Processor mProcessor;
     private Processor mSpyProcessor;
     private CommandInterceptingSystemDispatcher mDispatcher;
     private CommandInterceptingSystemDispatcher mSpyDispatcher;
-    private SystemAlarmDispatcher.CommandsCompletedListener mCompletedListener;
     private CountDownLatch mLatch;
 
-    private Trackers mTracker;
-    private BatteryChargingTracker mBatteryChargingTracker;
-    private BatteryNotLowTracker mBatteryNotLowTracker;
-    private NetworkStateTracker mNetworkStateTracker;
-    private StorageNotLowTracker mStorageNotLowTracker;
+    private FakeConstraintTracker mBatteryChargingTracker;
+    private FakeConstraintTracker mStorageNotLowTracker;
 
     @Before
+    @SuppressWarnings("unchecked")
     public void setUp() {
         mContext = ApplicationProvider.getApplicationContext().getApplicationContext();
-        mScheduler = mock(Scheduler.class);
+        Scheduler scheduler = mock(Scheduler.class);
         mWorkManager = mock(WorkManagerImpl.class);
         mLatch = new CountDownLatch(1);
-        mCompletedListener = new SystemAlarmDispatcher.CommandsCompletedListener() {
-            @Override
-            public void onAllCommandsCompleted() {
-                mLatch.countDown();
-            }
-        };
-        mTracker = mock(Trackers.class);
+        SystemAlarmDispatcher.CommandsCompletedListener completedListener =
+                new SystemAlarmDispatcher.CommandsCompletedListener() {
+                    @Override
+                    public void onAllCommandsCompleted() {
+                        mLatch.countDown();
+                    }
+                };
+
+        TaskExecutor instantTaskExecutor = new InstantWorkTaskExecutor();
+        mBatteryChargingTracker = new FakeConstraintTracker(mContext, instantTaskExecutor);
+        BatteryNotLowTracker batteryNotLowTracker =
+                new BatteryNotLowTracker(mContext, instantTaskExecutor);
+        // Requires API 24+ types.
+        ConstraintTracker<NetworkState> networkStateTracker =
+                new ConstraintTracker<NetworkState>(mContext, instantTaskExecutor) {
+                    @Override
+                    public NetworkState getInitialState() {
+                        return new NetworkState(true, true, true, true);
+                    }
+
+                    @Override
+                    public void startTracking() {
+                    }
+
+                    @Override
+                    public void stopTracking() {
+                    }
+                };
+        mStorageNotLowTracker = new FakeConstraintTracker(mContext, instantTaskExecutor);
+        Trackers trackers = new Trackers(mContext, instantTaskExecutor,
+                mBatteryChargingTracker, batteryNotLowTracker, networkStateTracker,
+                mStorageNotLowTracker);
         Logger.setLogger(new Logger.LogcatLogger(Log.DEBUG));
-        mConfiguration = new Configuration.Builder()
+        Configuration configuration = new Configuration.Builder()
                 .setExecutor(new SynchronousExecutor())
                 .build();
         when(mWorkManager.getWorkDatabase()).thenReturn(mDatabase);
-        when(mWorkManager.getConfiguration()).thenReturn(mConfiguration);
-        TaskExecutor instantTaskExecutor = new InstantWorkTaskExecutor();
+        when(mWorkManager.getConfiguration()).thenReturn(configuration);
         when(mWorkManager.getWorkTaskExecutor()).thenReturn(instantTaskExecutor);
-        when(mWorkManager.getTrackers()).thenReturn(mTracker);
-        mProcessor = new Processor(
+        when(mWorkManager.getTrackers()).thenReturn(trackers);
+        Processor processor = new Processor(
                 mContext,
-                mConfiguration,
+                configuration,
                 instantTaskExecutor,
                 mDatabase,
-                Collections.singletonList(mScheduler));
-        mSpyProcessor = spy(mProcessor);
+                Collections.singletonList(scheduler));
+        mSpyProcessor = spy(processor);
 
         mDispatcher =
                 new CommandInterceptingSystemDispatcher(mContext, mSpyProcessor, mWorkManager);
-        mDispatcher.setCompletedListener(mCompletedListener);
+        mDispatcher.setCompletedListener(completedListener);
         mSpyDispatcher = spy(mDispatcher);
-
-        mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext, instantTaskExecutor));
-        mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext, instantTaskExecutor));
-        // Requires API 24+ types.
-        mNetworkStateTracker = mock(NetworkStateTracker.class);
-        mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext, instantTaskExecutor));
-
-        when(mTracker.getBatteryChargingTracker()).thenReturn(mBatteryChargingTracker);
-        when(mTracker.getBatteryNotLowTracker()).thenReturn(mBatteryNotLowTracker);
-        when(mTracker.getNetworkStateTracker()).thenReturn(mNetworkStateTracker);
-        when(mTracker.getStorageNotLowTracker()).thenReturn(mStorageNotLowTracker);
     }
 
     @After
@@ -319,7 +325,7 @@
 
     @Test
     public void testSchedule_withConstraints() throws InterruptedException {
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+        mBatteryChargingTracker.setInitialState(true);
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(
                         System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1),
@@ -383,7 +389,7 @@
     @LargeTest
     @RepeatRule.Repeat(times = 1)
     public void testDelayMet_withUnMetConstraint() throws InterruptedException {
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
+        // fake BatteryCharging tracker says by default that it is not charging
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                 .setConstraints(new Constraints.Builder()
@@ -420,12 +426,12 @@
     @LargeTest
     @RepeatRule.Repeat(times = 1)
     public void testDelayMet_withPartiallyMetConstraint() throws InterruptedException {
-        when(mStorageNotLowTracker.getInitialState()).thenReturn(true);
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
+        mStorageNotLowTracker.setInitialState(true);
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                 .setConstraints(new Constraints.Builder()
                         .setRequiresStorageNotLow(true)
+                        // fake BatteryCharging tracker says by default that it is not charging
                         .setRequiresCharging(true)
                         .build())
                 .build();
@@ -457,7 +463,7 @@
 
     @Test
     public void testConstraintsChanged_withConstraint() throws InterruptedException {
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+        mBatteryChargingTracker.setInitialState(true);
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                 .setConstraints(new Constraints.Builder()
@@ -475,7 +481,7 @@
 
     @Test
     public void testDelayMet_withMetConstraint() throws InterruptedException {
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+        mBatteryChargingTracker.setInitialState(true);
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                 .setConstraints(new Constraints.Builder()
@@ -565,7 +571,7 @@
 
     @Test
     public void testConstraintsChanged_withFutureWork() throws InterruptedException {
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(true);
+        mBatteryChargingTracker.setInitialState(true);
         // Use a mocked scheduler in this test.
         Scheduler scheduler = mock(Scheduler.class);
         doCallRealMethod().when(mWorkManager).rescheduleEligibleWork();
@@ -654,10 +660,10 @@
     @RepeatRule.Repeat(times = 1)
     public void testDelayMet_withUnMetConstraintShouldNotCrashOnDestroy()
             throws InterruptedException {
-        when(mBatteryChargingTracker.getInitialState()).thenReturn(false);
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                 .setConstraints(new Constraints.Builder()
+                        // fake BatteryCharging tracker says by default that it is not charging
                         .setRequiresCharging(true)
                         .build())
                 .build();
@@ -720,4 +726,30 @@
             mCommands.add(intent);
         }
     }
+
+    private static final class FakeConstraintTracker extends ConstraintTracker<Boolean> {
+        private boolean mInitialState = false;
+
+        FakeConstraintTracker(@NonNull Context context,
+                @NonNull TaskExecutor taskExecutor) {
+            super(context, taskExecutor);
+        }
+
+        private void setInitialState(boolean initialState) {
+            mInitialState = initialState;
+        }
+
+        @Override
+        public Boolean getInitialState() {
+            return mInitialState;
+        }
+
+        @Override
+        public void startTracking() {
+        }
+
+        @Override
+        public void stopTracking() {
+        }
+    }
 }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
index 540925e..2d0d124 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
@@ -63,7 +63,7 @@
         workConstraintsTracker.replace(TEST_WORKSPECS)
         val (_, constrained) = capturingCallback.consumeCurrent()
         assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
-        tracker.setState(true)
+        tracker.state = true
         val (unconstrained, _) = capturingCallback.consumeCurrent()
         assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
     }
@@ -80,7 +80,7 @@
         )
         workConstraintsTracker.replace(TEST_WORKSPECS)
         capturingCallback.consumeCurrent()
-        tracker1.setState(true)
+        tracker1.state = true
         val (unconstrained, _) = capturingCallback.consumeCurrent()
         assertThat(unconstrained).containsExactly(TEST_WORKSPEC_IDS[0], TEST_WORKSPEC_IDS[1])
     }
@@ -92,7 +92,7 @@
         val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker1, tracker2)
         workConstraintsTracker.replace(TEST_WORKSPECS)
         capturingCallback.consumeCurrent()
-        tracker1.setState(true)
+        tracker1.state = true
         val (unconstrained, _) = capturingCallback.consumeCurrent()
         // only one constraint is resolved, so unconstrained is empty list
         assertThat(unconstrained).isEqualTo(emptyList<String>())
@@ -106,7 +106,7 @@
         workConstraintsTracker.replace(TEST_WORKSPECS)
         val (unconstrained, _) = capturingCallback.consumeCurrent()
         assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
-        tracker1.setState(false)
+        tracker1.state = false
         val (_, constrained) = capturingCallback.consumeCurrent()
         assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
     }
@@ -128,12 +128,11 @@
 }
 
 private class TestConstraintTracker(
-    val initialState: Boolean = false,
+    override val initialState: Boolean = false,
     context: Context = ApplicationProvider.getApplicationContext(),
     taskExecutor: TaskExecutor = InstantWorkTaskExecutor(),
 ) : ConstraintTracker<Boolean>(context, taskExecutor) {
     var isTracking = false
-    override fun getInitialState() = initialState
 
     override fun startTracking() {
         isTracking = true
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
index ffaa451..6b787c8 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
@@ -19,12 +19,14 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
 import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
@@ -33,6 +35,7 @@
 import androidx.work.WorkManagerTest;
 import androidx.work.impl.constraints.trackers.ConstraintTracker;
 import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
 import androidx.work.worker.TestWorker;
 
 import org.junit.Before;
@@ -46,14 +49,16 @@
 @SdkSuppress(minSdkVersion = 23)
 public class ConstraintControllerTest extends WorkManagerTest {
     private TestDeviceIdleConstraintController mTestIdleController;
+
     @SuppressWarnings("unchecked")
-    private ConstraintTracker<Boolean> mMockTracker = mock(ConstraintTracker.class);
+    private final FakeConstraintTracker mTracker = new FakeConstraintTracker();
+
     private ConstraintController.OnConstraintUpdatedCallback mCallback =
             mock(ConstraintController.OnConstraintUpdatedCallback.class);
 
     @Before
     public void setUp() {
-        mTestIdleController = new TestDeviceIdleConstraintController(mMockTracker);
+        mTestIdleController = new TestDeviceIdleConstraintController(mTracker);
         mTestIdleController.setCallback(mCallback);
     }
 
@@ -78,8 +83,8 @@
     @Test
     @SmallTest
     public void testReplace_empty() {
-        mTestIdleController.replace(Collections.<WorkSpec>emptyList());
-        verify(mMockTracker).removeListener(mTestIdleController);
+        mTestIdleController.replace(Collections.emptyList());
+        assertThat(mTracker.mTracking, is(false));
         verifyZeroInteractions(mCallback);
     }
 
@@ -89,7 +94,7 @@
         WorkSpec workSpecNoConstraints = createNoConstraintWorkSpec();
         List<WorkSpec> workSpecs = Collections.singletonList(workSpecNoConstraints);
         mTestIdleController.replace(workSpecs);
-        verify(mMockTracker).removeListener(mTestIdleController);
+        assertThat(mTracker.mTracking, is(false));
         verifyZeroInteractions(mCallback);
     }
 
@@ -100,10 +105,10 @@
         List<String> expectedWorkIds = Collections.singletonList(workSpecWithConstraint.id);
         List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
 
-        mTestIdleController.setDeviceActive();
+        mTracker.setDeviceActive();
         mTestIdleController.replace(workSpecs);
-        verify(mMockTracker).addListener(mTestIdleController);
-        verify(mCallback).onConstraintNotMet(eq(expectedWorkIds));
+        assertThat(mTracker.mTracking, is(true));
+        verify(mCallback, atLeastOnce()).onConstraintNotMet(eq(expectedWorkIds));
     }
 
     @Test
@@ -113,10 +118,13 @@
         List<String> expectedWorkIds = Collections.singletonList(workSpecWithConstraint.id);
         List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
 
-        mTestIdleController.setDeviceIdle();
+        mTracker.setDeviceIdle();
         mTestIdleController.replace(workSpecs);
-        verify(mMockTracker).addListener(mTestIdleController);
-        verify(mCallback).onConstraintMet(eq(expectedWorkIds));
+        assertThat(mTracker.mTracking, is(true));
+        // called twice: replace calls updateCallback explicitly and
+        // tracker.addListener results in updateCallback too
+        // probably should be fixed eventually
+        verify(mCallback, atLeastOnce()).onConstraintMet(eq(expectedWorkIds));
     }
 
     @Test
@@ -127,14 +135,17 @@
         List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
 
         mTestIdleController.replace(workSpecs);
-        verify(mCallback).onConstraintNotMet(expectedWorkIds);
+        // called twice: replace calls updateCallback explictily and
+        // tracker.addListener results in updateCallback too
+        // probably should be fixed eventually
+        verify(mCallback, atLeastOnce()).onConstraintNotMet(expectedWorkIds);
     }
 
     @Test
     @SmallTest
     public void testReset_alreadyNoMatchingWorkSpecs() {
         mTestIdleController.reset();
-        verifyZeroInteractions(mMockTracker);
+        assertThat(mTracker.mTracking, is(false));
     }
 
     @Test
@@ -145,7 +156,7 @@
         mTestIdleController.replace(workSpecs);
 
         mTestIdleController.reset();
-        verify(mMockTracker).removeListener(mTestIdleController);
+        assertThat(mTracker.mTracking, is(false));
     }
 
     @Test
@@ -162,11 +173,11 @@
         List<String> expectedWorkIds = Collections.singletonList(workSpecWithConstraint.id);
         List<WorkSpec> workSpecs = Collections.singletonList(workSpecWithConstraint);
         mTestIdleController.replace(workSpecs);
-        verify(mCallback).onConstraintNotMet(expectedWorkIds);
+        verify(mCallback, times(2)).onConstraintNotMet(expectedWorkIds);
 
         final boolean deviceIdle = false;
         mTestIdleController.onConstraintChanged(deviceIdle);
-        verify(mCallback, times(2)).onConstraintNotMet(expectedWorkIds);
+        verify(mCallback, times(3)).onConstraintNotMet(expectedWorkIds);
     }
 
     @Test
@@ -193,17 +204,8 @@
 
     @Test
     @SmallTest
-    public void testIsWorkSpecConstrained_constraintNotSet() {
-        WorkSpec workSpecWithConstraint = createTestConstraintWorkSpec();
-        mTestIdleController.replace(Collections.singletonList(workSpecWithConstraint));
-        assertThat(mTestIdleController.isWorkSpecConstrained(workSpecWithConstraint.id),
-                is(false));
-    }
-
-    @Test
-    @SmallTest
     public void testIsWorkSpecConstrained_constrained_withMatchingWorkSpecs() {
-        mTestIdleController.setDeviceActive();
+        mTracker.setDeviceActive();
 
         WorkSpec workSpecWithConstraint = createTestConstraintWorkSpec();
         mTestIdleController.replace(Collections.singletonList(workSpecWithConstraint));
@@ -214,7 +216,7 @@
     @Test
     @SmallTest
     public void testIsWorkSpecConstrained_constrained_noMatchingWorkSpecs() {
-        mTestIdleController.setDeviceActive();
+        mTracker.setDeviceActive();
 
         WorkSpec workSpecNoConstraints = createNoConstraintWorkSpec();
         mTestIdleController.replace(Collections.singletonList(workSpecNoConstraints));
@@ -225,7 +227,7 @@
     @Test
     @SmallTest
     public void testIsWorkSpecConstrained_unconstrained_withMatchingWorkSpecs() {
-        mTestIdleController.setDeviceIdle();
+        mTracker.setDeviceIdle();
 
         WorkSpec workSpecWithConstraint = createTestConstraintWorkSpec();
         mTestIdleController.replace(Collections.singletonList(workSpecWithConstraint));
@@ -236,7 +238,7 @@
     @Test
     @SmallTest
     public void testIsWorkSpecConstrained_unconstrained_noMatchingWorkSpecs() {
-        mTestIdleController.setDeviceIdle();
+        mTracker.setDeviceIdle();
 
         WorkSpec workSpecNoConstraints = createNoConstraintWorkSpec();
         mTestIdleController.replace(Collections.singletonList(workSpecNoConstraints));
@@ -258,13 +260,37 @@
         public boolean isConstrained(@NonNull Boolean isDeviceIdle) {
             return !isDeviceIdle;
         }
+    }
+
+    private static class FakeConstraintTracker extends ConstraintTracker<Boolean> {
+        public boolean mTracking;
+        private boolean mInitialState;
+
+        protected FakeConstraintTracker() {
+            super(ApplicationProvider.getApplicationContext(), new InstantWorkTaskExecutor());
+        }
+
+        @Override
+        public Boolean getInitialState() {
+            return mInitialState;
+        }
+
+        @Override
+        public void startTracking() {
+            mTracking = true;
+        }
+
+        @Override
+        public void stopTracking() {
+            mTracking = false;
+        }
 
         void setDeviceActive() {
-            onConstraintChanged(false);
+            mInitialState = false;
         }
 
         void setDeviceIdle() {
-            onConstraintChanged(true);
+            mInitialState = true;
         }
     }
 }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
index 6ac53c2..958e2ff 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryChargingTrackerTest.java
@@ -33,7 +33,6 @@
 import android.os.Build;
 
 import androidx.annotation.RequiresApi;
-import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
@@ -137,9 +136,7 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(true);
 
-        mTracker.onBroadcastReceive(
-                ApplicationProvider.getApplicationContext(),
-                new Intent("INVALID"));
+        mTracker.onBroadcastReceive(new Intent("INVALID"));
         verifyNoMoreInteractions(mListener);
     }
 
@@ -151,9 +148,9 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(false);
 
-        mTracker.onBroadcastReceive(mMockContext, createChargingIntent(true));
+        mTracker.onBroadcastReceive(createChargingIntent(true));
         verify(mListener).onConstraintChanged(true);
-        mTracker.onBroadcastReceive(mMockContext, createChargingIntent(false));
+        mTracker.onBroadcastReceive(createChargingIntent(false));
         verify(mListener, times(2)).onConstraintChanged(false);
     }
 
@@ -165,9 +162,9 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(false);
 
-        mTracker.onBroadcastReceive(mMockContext, createChargingIntent_afterApi23(true));
+        mTracker.onBroadcastReceive(createChargingIntent_afterApi23(true));
         verify(mListener).onConstraintChanged(true);
-        mTracker.onBroadcastReceive(mMockContext, createChargingIntent_afterApi23(false));
+        mTracker.onBroadcastReceive(createChargingIntent_afterApi23(false));
         // onConstraintChanged was called once more, in total, twice
         verify(mListener, times(2)).onConstraintChanged(false);
     }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
index d0c6333..cbf1e15 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/BatteryNotLowTrackerTest.java
@@ -46,7 +46,7 @@
 
     private static final int STATUS_CHARGING = BatteryManager.BATTERY_STATUS_CHARGING;
     private static final int UNKNOWN_STATUS = BatteryManager.BATTERY_STATUS_UNKNOWN;
-    private static final float BELOW_THRESHOLD = BatteryNotLowTracker.BATTERY_LOW_THRESHOLD;
+    private static final float BELOW_THRESHOLD = BatteryNotLowTrackerKt.BATTERY_LOW_THRESHOLD;
     private static final float ABOVE_THRESHOLD = BELOW_THRESHOLD + 0.01f;
 
     private Context mMockContext;
@@ -132,7 +132,7 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(true);
 
-        mTracker.onBroadcastReceive(mMockContext, new Intent("INVALID"));
+        mTracker.onBroadcastReceive(new Intent("INVALID"));
         verifyNoMoreInteractions(mListener);
     }
 
@@ -144,9 +144,9 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(false);
 
-        mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_BATTERY_OKAY));
+        mTracker.onBroadcastReceive(new Intent(Intent.ACTION_BATTERY_OKAY));
         verify(mListener).onConstraintChanged(true);
-        mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_BATTERY_LOW));
+        mTracker.onBroadcastReceive(new Intent(Intent.ACTION_BATTERY_LOW));
         verify(mListener, times(2)).onConstraintChanged(false);
     }
 }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
index 0313e43..c61c213 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/NetworkStateTrackerTest.java
@@ -15,6 +15,9 @@
  */
 package androidx.work.impl.constraints.trackers;
 
+import static androidx.work.impl.constraints.trackers.NetworkStateTrackerKt.NetworkStateTracker;
+import static androidx.work.impl.constraints.trackers.NetworkStateTrackerKt.isActiveNetworkValidated;
+
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.ArgumentMatchers.any;
@@ -44,7 +47,7 @@
 @RunWith(AndroidJUnit4.class)
 public class NetworkStateTrackerTest {
 
-    private NetworkStateTracker mTracker;
+    private ConstraintTracker<NetworkState> mTracker;
 
     private Context mMockContext;
     private ConnectivityManager mMockConnectivityManager;
@@ -57,8 +60,7 @@
         when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
         when(mMockContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE)))
                 .thenReturn(mMockConnectivityManager);
-
-        mTracker = new NetworkStateTracker(mMockContext, new InstantWorkTaskExecutor());
+        mTracker = NetworkStateTracker(mMockContext, new InstantWorkTaskExecutor());
     }
 
     @Test
@@ -125,6 +127,6 @@
         when(mMockConnectivityManager.getActiveNetwork()).thenReturn(activeNetwork);
         when(mMockConnectivityManager.getNetworkCapabilities(activeNetwork))
                 .thenThrow(new SecurityException("Exception"));
-        assertThat(mTracker.isActiveNetworkValidated(), is(false));
+        assertThat(isActiveNetworkValidated(mMockConnectivityManager), is(false));
     }
 }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
index 3f71de9..95d0d18 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/trackers/StorageNotLowTrackerTest.java
@@ -98,7 +98,7 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(true);
 
-        mTracker.onBroadcastReceive(mMockContext, new Intent("INVALID"));
+        mTracker.onBroadcastReceive(new Intent("INVALID"));
         verifyNoMoreInteractions(mListener);
     }
 
@@ -109,9 +109,9 @@
         mTracker.addListener(mListener);
         verify(mListener).onConstraintChanged(false);
 
-        mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_DEVICE_STORAGE_OK));
+        mTracker.onBroadcastReceive(new Intent(Intent.ACTION_DEVICE_STORAGE_OK));
         verify(mListener).onConstraintChanged(true);
-        mTracker.onBroadcastReceive(mMockContext, new Intent(Intent.ACTION_DEVICE_STORAGE_LOW));
+        mTracker.onBroadcastReceive(new Intent(Intent.ACTION_DEVICE_STORAGE_LOW));
         // onConstraintChanged was called once more, in total, twice
         verify(mListener, times(2)).onConstraintChanged(false);
     }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index ae17700..532c261 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -52,9 +52,10 @@
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.WorkerWrapper;
+import androidx.work.impl.constraints.NetworkState;
 import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
 import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
+import androidx.work.impl.constraints.trackers.ConstraintTracker;
 import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
 import androidx.work.impl.constraints.trackers.Trackers;
 import androidx.work.impl.foreground.ForegroundProcessor;
@@ -108,13 +109,14 @@
     private Trackers mTracker;
     private BatteryChargingTracker mBatteryChargingTracker;
     private BatteryNotLowTracker mBatteryNotLowTracker;
-    private NetworkStateTracker mNetworkStateTracker;
+    private ConstraintTracker<NetworkState> mNetworkStateTracker;
     private StorageNotLowTracker mStorageNotLowTracker;
 
     @Rule
     public final RepeatRule mRepeatRule = new RepeatRule();
 
     @Before
+    @SuppressWarnings("unchecked")
     public void setUp() {
         mContext = ApplicationProvider.getApplicationContext().getApplicationContext();
         mHandlerThread = new HandlerThread("ConstraintTrackingHandler");
@@ -140,7 +142,7 @@
         mBatteryChargingTracker = spy(new BatteryChargingTracker(mContext, mWorkTaskExecutor));
         mBatteryNotLowTracker = spy(new BatteryNotLowTracker(mContext, mWorkTaskExecutor));
         // Requires API 24+ types.
-        mNetworkStateTracker = mock(NetworkStateTracker.class);
+        mNetworkStateTracker = mock(ConstraintTracker.class);
         mStorageNotLowTracker = spy(new StorageNotLowTracker(mContext, mWorkTaskExecutor));
         mTracker = mock(Trackers.class);
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 0853d9d..acb17785 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -269,7 +269,7 @@
             @NonNull WorkDatabase database) {
         Context applicationContext = context.getApplicationContext();
         Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
-        mTrackers = new Trackers(applicationContext, mWorkTaskExecutor);
+        mTrackers = new Trackers(applicationContext, workTaskExecutor);
         List<Scheduler> schedulers =
                 createSchedulers(applicationContext, configuration, mTrackers);
         Processor processor = new Processor(
@@ -300,7 +300,7 @@
             @NonNull WorkDatabase workDatabase,
             @NonNull List<Scheduler> schedulers,
             @NonNull Processor processor) {
-        mTrackers = new Trackers(context.getApplicationContext(), mWorkTaskExecutor);
+        mTrackers = new Trackers(context.getApplicationContext(), workTaskExecutor);
         internalInit(context, configuration, workTaskExecutor, workDatabase, schedulers, processor);
     }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
index c72820c..559a5b1 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
@@ -22,16 +22,14 @@
 import androidx.work.NetworkType.TEMPORARILY_UNMETERED
 import androidx.work.NetworkType.UNMETERED
 import androidx.work.impl.constraints.NetworkState
-import androidx.work.impl.constraints.trackers.BatteryChargingTracker
 import androidx.work.impl.constraints.trackers.BatteryNotLowTracker
-import androidx.work.impl.constraints.trackers.NetworkStateTracker
-import androidx.work.impl.constraints.trackers.StorageNotLowTracker
+import androidx.work.impl.constraints.trackers.ConstraintTracker
 import androidx.work.impl.model.WorkSpec
 
 /**
  * A [ConstraintController] for battery charging events.
  */
-class BatteryChargingController(tracker: BatteryChargingTracker) :
+class BatteryChargingController(tracker: ConstraintTracker<Boolean>) :
     ConstraintController<Boolean>(tracker) {
     override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresCharging()
 
@@ -51,7 +49,7 @@
 /**
  * A [ConstraintController] for monitoring that the network connection is unmetered.
  */
-class NetworkUnmeteredController(tracker: NetworkStateTracker) :
+class NetworkUnmeteredController(tracker: ConstraintTracker<NetworkState>) :
     ConstraintController<NetworkState>(tracker) {
     override fun hasConstraint(workSpec: WorkSpec): Boolean {
         val requiredNetworkType = workSpec.constraints.requiredNetworkType
@@ -65,7 +63,7 @@
 /**
  * A [ConstraintController] for storage not low events.
  */
-class StorageNotLowController(tracker: StorageNotLowTracker) :
+class StorageNotLowController(tracker: ConstraintTracker<Boolean>) :
     ConstraintController<Boolean>(tracker) {
 
     override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresStorageNotLow()
@@ -76,7 +74,7 @@
 /**
  * A [ConstraintController] for monitoring that the network connection is not roaming.
  */
-class NetworkNotRoamingController(tracker: NetworkStateTracker) :
+class NetworkNotRoamingController(tracker: ConstraintTracker<NetworkState>) :
     ConstraintController<NetworkState>(tracker) {
     override fun hasConstraint(workSpec: WorkSpec): Boolean {
         return workSpec.constraints.requiredNetworkType == NetworkType.NOT_ROAMING
@@ -111,7 +109,7 @@
  *
  * For API 25 and below, usable simply means that [NetworkState] is connected.
  */
-class NetworkConnectedController(tracker: NetworkStateTracker) :
+class NetworkConnectedController(tracker: ConstraintTracker<NetworkState>) :
     ConstraintController<NetworkState>(tracker) {
     override fun hasConstraint(workSpec: WorkSpec) =
         workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED
@@ -127,7 +125,7 @@
 /**
  * A [ConstraintController] for monitoring that the network connection is metered.
  */
-class NetworkMeteredController(tracker: NetworkStateTracker) :
+class NetworkMeteredController(tracker: ConstraintTracker<NetworkState>) :
     ConstraintController<NetworkState>(tracker) {
     override fun hasConstraint(workSpec: WorkSpec) =
         workSpec.constraints.requiredNetworkType == NetworkType.METERED
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
deleted file mode 100644
index 1128d24..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * Tracks whether or not the device's battery is charging.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class BatteryChargingTracker extends BroadcastReceiverConstraintTracker<Boolean> {
-
-    private static final String TAG = Logger.tagWithPrefix("BatteryChrgTracker");
-
-    /**
-     * Create an instance of {@link BatteryChargingTracker}.
-     * @param context The application {@link Context}
-     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
-     */
-    public BatteryChargingTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
-        super(context, taskExecutor);
-    }
-
-    @Override
-    public Boolean getInitialState() {
-        // {@link ACTION_CHARGING} and {@link ACTION_DISCHARGING} are not sticky broadcasts, so
-        // we use {@link ACTION_BATTERY_CHANGED} on all APIs to get the initial state.
-        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
-        Intent intent = mAppContext.registerReceiver(null, intentFilter);
-        if (intent == null) {
-            Logger.get().error(TAG, "getInitialState - null intent received");
-            return false;
-        }
-        return isBatteryChangedIntentCharging(intent);
-    }
-
-    @Override
-    public IntentFilter getIntentFilter() {
-        IntentFilter intentFilter = new IntentFilter();
-        if (Build.VERSION.SDK_INT >= 23) {
-            intentFilter.addAction(BatteryManager.ACTION_CHARGING);
-            intentFilter.addAction(BatteryManager.ACTION_DISCHARGING);
-        } else {
-            intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
-            intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
-        }
-        return intentFilter;
-    }
-
-    @Override
-    public void onBroadcastReceive(Context context, @NonNull Intent intent) {
-        String action = intent.getAction();
-        if (action == null) {
-            return;
-        }
-
-        Logger.get().debug(TAG, "Received " + action);
-        switch (action) {
-            case BatteryManager.ACTION_CHARGING:
-                setState(true);
-                break;
-
-            case BatteryManager.ACTION_DISCHARGING:
-                setState(false);
-                break;
-
-            case Intent.ACTION_POWER_CONNECTED:
-                setState(true);
-                break;
-
-            case Intent.ACTION_POWER_DISCONNECTED:
-                setState(false);
-                break;
-        }
-    }
-
-    private boolean isBatteryChangedIntentCharging(Intent intent) {
-        boolean charging;
-        if (Build.VERSION.SDK_INT >= 23) {
-            int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
-            charging = (status == BatteryManager.BATTERY_STATUS_CHARGING
-                    || status == BatteryManager.BATTERY_STATUS_FULL);
-        } else {
-            int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
-            charging = (chargePlug != 0);
-        }
-        return charging;
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.kt
new file mode 100644
index 0000000..9a59363
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryChargingTracker.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.BatteryManager
+import android.os.BatteryManager.BATTERY_STATUS_CHARGING
+import android.os.BatteryManager.BATTERY_STATUS_FULL
+import android.os.Build
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * Tracks whether or not the device's battery is charging.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class BatteryChargingTracker(context: Context, taskExecutor: TaskExecutor) :
+    BroadcastReceiverConstraintTracker<Boolean>(context, taskExecutor) {
+
+    override val initialState: Boolean
+        get() {
+            // {@link ACTION_CHARGING} and {@link ACTION_DISCHARGING} are not sticky broadcasts, so
+            // we use {@link ACTION_BATTERY_CHANGED} on all APIs to get the initial state.
+            val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+            val intent = appContext.registerReceiver(null, intentFilter)
+            if (intent == null) {
+                Logger.get().error(TAG, "getInitialState - null intent received")
+                return false
+            }
+            return isBatteryChangedIntentCharging(intent)
+        }
+
+    override val intentFilter: IntentFilter
+        get() {
+            val intentFilter = IntentFilter()
+            if (Build.VERSION.SDK_INT >= 23) {
+                intentFilter.addAction(BatteryManager.ACTION_CHARGING)
+                intentFilter.addAction(BatteryManager.ACTION_DISCHARGING)
+            } else {
+                intentFilter.addAction(Intent.ACTION_POWER_CONNECTED)
+                intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED)
+            }
+            return intentFilter
+        }
+
+    override fun onBroadcastReceive(intent: Intent) {
+        val action = intent.action ?: return
+        Logger.get().debug(TAG, "Received $action")
+        when (action) {
+            BatteryManager.ACTION_CHARGING -> state = true
+            BatteryManager.ACTION_DISCHARGING -> state = false
+            Intent.ACTION_POWER_CONNECTED -> state = true
+            Intent.ACTION_POWER_DISCONNECTED -> state = false
+        }
+    }
+
+    private fun isBatteryChangedIntentCharging(intent: Intent): Boolean {
+        return if (Build.VERSION.SDK_INT >= 23) {
+            val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+            (status == BATTERY_STATUS_CHARGING || status == BATTERY_STATUS_FULL)
+        } else {
+            intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
+        }
+    }
+}
+
+private val TAG = Logger.tagWithPrefix("BatteryChrgTracker")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
deleted file mode 100644
index a8e4326..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * Tracks whether or not the device's battery level is low.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class BatteryNotLowTracker extends BroadcastReceiverConstraintTracker<Boolean> {
-
-    private static final String TAG = Logger.tagWithPrefix("BatteryNotLowTracker");
-
-    /**
-     * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/res/res/values/config.xml#986}
-     */
-    static final float BATTERY_LOW_THRESHOLD = 0.15f;
-
-    /**
-     * Create an instance of {@link BatteryNotLowTracker}.
-     * @param context The application {@link Context}
-     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
-     */
-    public BatteryNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
-        super(context, taskExecutor);
-    }
-
-    /**
-     * Based on BatteryService#shouldSendBatteryLowLocked(), but this ignores the previous plugged
-     * state - cannot guarantee the last plugged state because this isn't always tracking.
-     *
-     * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/BatteryService.java#268}
-     */
-    @Override
-    public Boolean getInitialState() {
-        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
-        Intent intent = mAppContext.registerReceiver(null, intentFilter);
-        if (intent == null) {
-            Logger.get().error(TAG, "getInitialState - null intent received");
-            return false;
-        }
-
-        int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
-        int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
-        int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
-        float batteryPercentage = level / (float) scale;
-
-        // BATTERY_STATUS_UNKNOWN typically refers to devices without a battery.
-        // So those kinds of devices must be allowed.
-        return (status == BatteryManager.BATTERY_STATUS_UNKNOWN
-                || batteryPercentage > BATTERY_LOW_THRESHOLD);
-    }
-
-    @Override
-    public IntentFilter getIntentFilter() {
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(Intent.ACTION_BATTERY_OKAY);
-        intentFilter.addAction(Intent.ACTION_BATTERY_LOW);
-        return intentFilter;
-    }
-
-    @Override
-    public void onBroadcastReceive(Context context, @NonNull Intent intent) {
-        if (intent.getAction() == null) {
-            return;
-        }
-
-        Logger.get().debug(TAG, "Received " + intent.getAction());
-
-        switch (intent.getAction()) {
-            case Intent.ACTION_BATTERY_OKAY:
-                setState(true);
-                break;
-
-            case Intent.ACTION_BATTERY_LOW:
-                setState(false);
-                break;
-        }
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.kt
new file mode 100644
index 0000000..7b98bc1
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BatteryNotLowTracker.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.IntentFilter
+import android.content.Intent
+import android.os.BatteryManager
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * Tracks whether or not the device's battery level is low.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class BatteryNotLowTracker(context: Context, taskExecutor: TaskExecutor) :
+    BroadcastReceiverConstraintTracker<Boolean>(context, taskExecutor) {
+    /**
+     * Based on BatteryService#shouldSendBatteryLowLocked(), but this ignores the previous plugged
+     * state - cannot guarantee the last plugged state because this isn't always tracking.
+     *
+     * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/BatteryService.java#268}
+     */
+    override val initialState: Boolean
+        get() {
+            val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+            val intent = appContext.registerReceiver(null, intentFilter)
+            if (intent == null) {
+                Logger.get().error(TAG, "getInitialState - null intent received")
+                return false
+            }
+            val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+            val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
+            val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
+            val batteryPercentage = level / scale.toFloat()
+
+            // BATTERY_STATUS_UNKNOWN typically refers to devices without a battery.
+            // So those kinds of devices must be allowed.
+            return status == BatteryManager.BATTERY_STATUS_UNKNOWN ||
+                batteryPercentage > BATTERY_LOW_THRESHOLD
+        }
+
+    override val intentFilter: IntentFilter
+        get() {
+            val intentFilter = IntentFilter()
+            intentFilter.addAction(Intent.ACTION_BATTERY_OKAY)
+            intentFilter.addAction(Intent.ACTION_BATTERY_LOW)
+            return intentFilter
+        }
+
+    override fun onBroadcastReceive(intent: Intent) {
+        if (intent.action == null) {
+            return
+        }
+        Logger.get().debug(TAG, "Received ${intent.action}")
+        when (intent.action) {
+            Intent.ACTION_BATTERY_OKAY -> state = true
+            Intent.ACTION_BATTERY_LOW -> state = false
+        }
+    }
+}
+
+private val TAG = Logger.tagWithPrefix("BatteryNotLowTracker")
+
+/**
+ * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/res/res/values/config.xml#986}
+ */
+internal const val BATTERY_LOW_THRESHOLD = 0.15f
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
deleted file mode 100644
index ecfe937..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work.impl.constraints.trackers;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * A {@link ConstraintTracker} with a {@link BroadcastReceiver} for monitoring constraint changes.
- *
- * @param <T> the constraint data type observed by this tracker
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class BroadcastReceiverConstraintTracker<T> extends ConstraintTracker<T> {
-    private static final String TAG = Logger.tagWithPrefix("BrdcstRcvrCnstrntTrckr");
-
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (intent != null) {
-                onBroadcastReceive(context, intent);
-            }
-        }
-    };
-
-    public BroadcastReceiverConstraintTracker(
-            @NonNull Context context,
-            @NonNull TaskExecutor taskExecutor) {
-        super(context, taskExecutor);
-    }
-
-    /**
-     * Called when the {@link BroadcastReceiver} is receiving an {@link Intent} broadcast and should
-     * handle the received {@link Intent}.
-     *
-     * @param context The {@link Context} in which the receiver is running.
-     * @param intent  The {@link Intent} being received.
-     */
-    public abstract void onBroadcastReceive(Context context, @NonNull Intent intent);
-
-    /**
-     * @return The {@link IntentFilter} associated with this tracker.
-     */
-    public abstract IntentFilter getIntentFilter();
-
-    @Override
-    public void startTracking() {
-        Logger.get().debug(TAG, getClass().getSimpleName() + ": registering receiver");
-        mAppContext.registerReceiver(mBroadcastReceiver, getIntentFilter());
-    }
-
-    @Override
-    public void stopTracking() {
-        Logger.get().debug(
-                TAG,
-                getClass().getSimpleName() + ": unregistering receiver");
-        mAppContext.unregisterReceiver(mBroadcastReceiver);
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.kt
new file mode 100644
index 0000000..7bdd924
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/BroadcastReceiverConstraintTracker.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * A [ConstraintTracker] with a [BroadcastReceiver] for monitoring constraint changes.
+ *
+ * @param <T> the constraint data type observed by this tracker
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class BroadcastReceiverConstraintTracker<T>(
+    context: Context,
+    taskExecutor: TaskExecutor
+) : ConstraintTracker<T>(context, taskExecutor) {
+    private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            onBroadcastReceive(intent)
+        }
+    }
+
+    /**
+     * Called when the [BroadcastReceiver] is receiving an [Intent] broadcast and should
+     * handle the received [Intent].
+     *
+     * @param intent  The [Intent] being received.
+     */
+    abstract fun onBroadcastReceive(intent: Intent)
+
+    /**
+     * @return The [IntentFilter] associated with this tracker.
+     */
+    abstract val intentFilter: IntentFilter
+
+    override fun startTracking() {
+        Logger.get().debug(TAG, "${javaClass.simpleName}: registering receiver")
+        appContext.registerReceiver(broadcastReceiver, intentFilter)
+    }
+
+    override fun stopTracking() {
+        Logger.get().debug(TAG, "${javaClass.simpleName}: unregistering receiver")
+        appContext.unregisterReceiver(broadcastReceiver)
+    }
+}
+private val TAG = Logger.tagWithPrefix("BrdcstRcvrCnstrntTrckr")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
deleted file mode 100644
index 935d3ab..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.ConstraintListener;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * A base for tracking constraints and notifying listeners of changes.
- *
- * @param <T> the constraint data type observed by this tracker
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class ConstraintTracker<T> {
-
-    private static final String TAG = Logger.tagWithPrefix("ConstraintTracker");
-
-    protected final TaskExecutor mTaskExecutor;
-    protected final Context mAppContext;
-
-    private final Object mLock = new Object();
-    private final Set<ConstraintListener<T>> mListeners = new LinkedHashSet<>();
-
-    // Synthetic access
-    T mCurrentState;
-
-    protected ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
-        mAppContext = context.getApplicationContext();
-        mTaskExecutor = taskExecutor;
-    }
-
-    /**
-     * Add the given listener for tracking.
-     * This may cause {@link #getInitialState()} and {@link #startTracking()} to be invoked.
-     * If a state is set, this will immediately notify the given listener.
-     *
-     * @param listener The target listener to start notifying
-     */
-    public void addListener(ConstraintListener<T> listener) {
-        synchronized (mLock) {
-            if (mListeners.add(listener)) {
-                if (mListeners.size() == 1) {
-                    mCurrentState = getInitialState();
-                    Logger.get().debug(TAG,
-                            getClass().getSimpleName() + ": initial state = " + mCurrentState);
-                    startTracking();
-                }
-                listener.onConstraintChanged(mCurrentState);
-            }
-        }
-    }
-
-    /**
-     * Remove the given listener from tracking.
-     *
-     * @param listener The listener to stop notifying.
-     */
-    public void removeListener(ConstraintListener<T> listener) {
-        synchronized (mLock) {
-            if (mListeners.remove(listener) && mListeners.isEmpty()) {
-                stopTracking();
-            }
-        }
-    }
-
-    /**
-     * Sets the state of the constraint.
-     * If state is has not changed, nothing happens.
-     *
-     * @param newState new state of constraint
-     */
-    public void setState(T newState) {
-        synchronized (mLock) {
-            if (mCurrentState == newState
-                    || (mCurrentState != null && mCurrentState.equals(newState))) {
-                return;
-            }
-            mCurrentState = newState;
-
-            // onConstraintChanged may lead to calls to addListener or removeListener.
-            // This can potentially result in a modification to the set while it is being
-            // iterated over, so we handle this by creating a copy and using that for
-            // iteration.
-            final List<ConstraintListener<T>> listenersList = new ArrayList<>(mListeners);
-            mTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
-                @Override
-                public void run() {
-                    for (ConstraintListener<T> listener : listenersList) {
-                        listener.onConstraintChanged(mCurrentState);
-                    }
-                }
-            });
-        }
-    }
-
-    /**
-     * Determines the initial state of the constraint being tracked.
-     */
-    public abstract T getInitialState();
-
-    /**
-     * Start tracking for constraint state changes.
-     */
-    public abstract void startTracking();
-
-    /**
-     * Stop tracking for constraint state changes.
-     */
-    public abstract void stopTracking();
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.kt
new file mode 100644
index 0000000..403d254
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.constraints.ConstraintListener
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import java.util.LinkedHashSet
+
+/**
+ * A base for tracking constraints and notifying listeners of changes.
+ *
+ * @param <T> the constraint data type observed by this tracker
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class ConstraintTracker<T> protected constructor(
+    context: Context,
+    private val taskExecutor: TaskExecutor
+) {
+    protected val appContext: Context = context.applicationContext
+    private val lock = Any()
+    private val listeners = LinkedHashSet<ConstraintListener<T>>()
+
+    private var currentState: T? = null
+
+    /**
+     * Add the given listener for tracking.
+     * This may cause [.getInitialState] and [.startTracking] to be invoked.
+     * If a state is set, this will immediately notify the given listener.
+     *
+     * @param listener The target listener to start notifying
+     */
+    fun addListener(listener: ConstraintListener<T>) {
+        synchronized(lock) {
+            if (listeners.add(listener)) {
+                if (listeners.size == 1) {
+                    currentState = initialState
+                    Logger.get().debug(
+                        TAG, "${javaClass.simpleName}: initial state = $currentState"
+                    )
+                    startTracking()
+                }
+                @Suppress("UNCHECKED_CAST")
+                listener.onConstraintChanged(currentState as T)
+            }
+        }
+    }
+
+    /**
+     * Remove the given listener from tracking.
+     *
+     * @param listener The listener to stop notifying.
+     */
+    fun removeListener(listener: ConstraintListener<T>) {
+        synchronized(lock) {
+            if (listeners.remove(listener) && listeners.isEmpty()) {
+                stopTracking()
+            }
+        }
+    }
+
+    var state: T
+        get() {
+            return currentState ?: initialState
+        }
+
+        set(newState) {
+            synchronized(lock) {
+                if (currentState != null && (currentState == newState)) {
+                    return
+                }
+
+                currentState = newState
+
+                // onConstraintChanged may lead to calls to addListener or removeListener.
+                // This can potentially result in a modification to the set while it is being
+                // iterated over, so we handle this by creating a copy and using that for
+                // iteration.
+                val listenersList = listeners.toList()
+                taskExecutor.mainThreadExecutor.execute {
+                    listenersList.forEach { listener ->
+                        // currentState was initialized by now
+                        @Suppress("UNCHECKED_CAST")
+                        listener.onConstraintChanged(currentState as T)
+                    }
+                }
+            }
+        }
+
+    /**
+     * Determines the initial state of the constraint being tracked.
+     */
+    abstract val initialState: T
+
+    /**
+     * Start tracking for constraint state changes.
+     */
+    abstract fun startTracking()
+
+    /**
+     * Stop tracking for constraint state changes.
+     */
+    abstract fun stopTracking()
+}
+
+private val TAG = Logger.tagWithPrefix("ConstraintTracker")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
deleted file mode 100644
index ce56137..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.trackers;
-
-import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.net.ConnectivityManagerCompat;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.utils.NetworkApi21;
-import androidx.work.impl.utils.NetworkApi23;
-import androidx.work.impl.utils.NetworkApi24;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * A {@link ConstraintTracker} for monitoring network state.
- * <p>
- * For API 24 and up: Network state is tracked using a registered {@link NetworkCallback} with
- * {@link ConnectivityManager#registerDefaultNetworkCallback(NetworkCallback)}, added in API 24.
- * <p>
- * For API 23 and below: Network state is tracked using a {@link android.content.BroadcastReceiver}.
- * Much less efficient than tracking with {@link NetworkCallback}s and {@link ConnectivityManager}.
- * <p>
- * Based on {@link android.app.job.JobScheduler}'s ConnectivityController on API 26.
- * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/job/controllers/ConnectivityController.java}
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class NetworkStateTracker extends ConstraintTracker<NetworkState> {
-
-    // Synthetic Accessor
-    static final String TAG = Logger.tagWithPrefix("NetworkStateTracker");
-
-    private final ConnectivityManager mConnectivityManager;
-
-    @RequiresApi(24)
-    private NetworkStateCallback mNetworkCallback;
-    private NetworkStateBroadcastReceiver mBroadcastReceiver;
-
-    /**
-     * Create an instance of {@link NetworkStateTracker}
-     * @param context the application {@link Context}
-     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
-     */
-    public NetworkStateTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
-        super(context, taskExecutor);
-        mConnectivityManager =
-                (ConnectivityManager) mAppContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-        if (isNetworkCallbackSupported()) {
-            mNetworkCallback = new NetworkStateCallback();
-        } else {
-            mBroadcastReceiver = new NetworkStateBroadcastReceiver();
-        }
-    }
-
-    @Override
-    public NetworkState getInitialState() {
-        return getActiveNetworkState();
-    }
-
-    @Override
-    public void startTracking() {
-        if (isNetworkCallbackSupported()) {
-            try {
-                Logger.get().debug(TAG, "Registering network callback");
-                NetworkApi24.registerDefaultNetworkCallbackCompat(mConnectivityManager,
-                        mNetworkCallback);
-            } catch (IllegalArgumentException | SecurityException e) {
-                // Catching the exceptions since and moving on - this tracker is only used for
-                // GreedyScheduler and there is nothing to be done about device-specific bugs.
-                // IllegalStateException: Happening on NVIDIA Shield K1 Tablets.  See b/136569342.
-                // SecurityException: Happening on Solone W1450.  See b/153246136.
-                Logger.get().error(
-                        TAG,
-                        "Received exception while registering network callback",
-                        e);
-            }
-        } else {
-            Logger.get().debug(TAG, "Registering broadcast receiver");
-            mAppContext.registerReceiver(mBroadcastReceiver,
-                    new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-        }
-    }
-
-    @Override
-    public void stopTracking() {
-        if (isNetworkCallbackSupported()) {
-            try {
-                Logger.get().debug(TAG, "Unregistering network callback");
-                NetworkApi21.unregisterNetworkCallbackCompat(mConnectivityManager,
-                        mNetworkCallback);
-            } catch (IllegalArgumentException | SecurityException e) {
-                // Catching the exceptions since and moving on - this tracker is only used for
-                // GreedyScheduler and there is nothing to be done about device-specific bugs.
-                // IllegalStateException: Happening on NVIDIA Shield K1 Tablets.  See b/136569342.
-                // SecurityException: Happening on Solone W1450.  See b/153246136.
-                Logger.get().error(
-                        TAG,
-                        "Received exception while unregistering network callback",
-                        e);
-            }
-        } else {
-            Logger.get().debug(TAG, "Unregistering broadcast receiver");
-            mAppContext.unregisterReceiver(mBroadcastReceiver);
-        }
-    }
-
-    private static boolean isNetworkCallbackSupported() {
-        // Based on requiring ConnectivityManager#registerDefaultNetworkCallback - added in API 24.
-        return Build.VERSION.SDK_INT >= 24;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    NetworkState getActiveNetworkState() {
-        // Use getActiveNetworkInfo() instead of getNetworkInfo(network) because it can detect VPNs.
-        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
-        boolean isConnected = info != null && info.isConnected();
-        boolean isValidated = isActiveNetworkValidated();
-        boolean isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager);
-        boolean isNotRoaming = info != null && !info.isRoaming();
-        return new NetworkState(isConnected, isValidated, isMetered, isNotRoaming);
-    }
-
-    @VisibleForTesting
-    boolean isActiveNetworkValidated() {
-        if (Build.VERSION.SDK_INT < 23) {
-            return false; // NET_CAPABILITY_VALIDATED not available until API 23. Used on API 26+.
-        }
-        try {
-            Network network = NetworkApi23.getActiveNetworkCompat(mConnectivityManager);
-            NetworkCapabilities capabilities =
-                    NetworkApi21.getNetworkCapabilitiesCompat(mConnectivityManager, network);
-            return capabilities != null
-                    && NetworkApi21.hasCapabilityCompat(capabilities, NET_CAPABILITY_VALIDATED);
-        } catch (SecurityException exception) {
-            // b/163342798
-            Logger.get().error(TAG, "Unable to validate active network", exception);
-            return false;
-        }
-    }
-
-    @RequiresApi(24)
-    private class NetworkStateCallback extends NetworkCallback {
-        NetworkStateCallback() {
-        }
-
-        @Override
-        public void onCapabilitiesChanged(
-                @NonNull Network network, @NonNull NetworkCapabilities capabilities) {
-            // The Network parameter is unreliable when a VPN app is running - use active network.
-            Logger.get().debug(
-                    TAG,
-                    "Network capabilities changed: " + capabilities);
-            setState(getActiveNetworkState());
-        }
-
-        @Override
-        public void onLost(@NonNull Network network) {
-            Logger.get().debug(TAG, "Network connection lost");
-            setState(getActiveNetworkState());
-        }
-    }
-
-    private class NetworkStateBroadcastReceiver extends BroadcastReceiver {
-        NetworkStateBroadcastReceiver() {
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (intent == null || intent.getAction() == null) {
-                return;
-            }
-            if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-                Logger.get().debug(TAG, "Network broadcast received");
-                setState(getActiveNetworkState());
-            }
-        }
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt
new file mode 100644
index 0000000..07f3975
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.core.net.ConnectivityManagerCompat
+import androidx.work.Logger
+import androidx.work.impl.constraints.NetworkState
+import androidx.work.impl.utils.getActiveNetworkCompat
+import androidx.work.impl.utils.getNetworkCapabilitiesCompat
+import androidx.work.impl.utils.hasCapabilityCompat
+import androidx.work.impl.utils.registerDefaultNetworkCallbackCompat
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.unregisterNetworkCallbackCompat
+
+/**
+ * A [ConstraintTracker] for monitoring network state.
+ *
+ *
+ * For API 24 and up: Network state is tracked using a registered [NetworkCallback] with
+ * [ConnectivityManager.registerDefaultNetworkCallback], added in API 24.
+ *
+ *
+ * For API 23 and below: Network state is tracked using a [android.content.BroadcastReceiver].
+ * Much less efficient than tracking with [NetworkCallback]s and [ConnectivityManager].
+ *
+ *
+ * Based on [android.app.job.JobScheduler]'s ConnectivityController on API 26.
+ * {@see https://android.googlesource.com/platform/frameworks/base/+/oreo-release/services/core/java/com/android/server/job/controllers/ConnectivityController.java}
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun NetworkStateTracker(
+    context: Context,
+    taskExecutor: TaskExecutor
+): ConstraintTracker<NetworkState> {
+    // Based on requiring ConnectivityManager#registerDefaultNetworkCallback - added in API 24.
+    return if (Build.VERSION.SDK_INT >= 24) {
+        NetworkStateTracker24(context, taskExecutor)
+    } else {
+        NetworkStateTrackerPre24(context, taskExecutor)
+    }
+}
+
+private val TAG = Logger.tagWithPrefix("NetworkStateTracker")
+
+internal val ConnectivityManager.isActiveNetworkValidated: Boolean
+    get() = if (Build.VERSION.SDK_INT < 23) {
+        false // NET_CAPABILITY_VALIDATED not available until API 23. Used on API 26+.
+    } else try {
+        val network = getActiveNetworkCompat()
+        val capabilities = getNetworkCapabilitiesCompat(network)
+        (capabilities?.hasCapabilityCompat(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) ?: false
+    } catch (exception: SecurityException) {
+        // b/163342798
+        Logger.get().error(TAG, "Unable to validate active network", exception)
+        false
+    }
+
+@Suppress("DEPRECATION")
+internal val ConnectivityManager.activeNetworkState: NetworkState
+    get() {
+        // Use getActiveNetworkInfo() instead of getNetworkInfo(network) because it can detect VPNs.
+        val info = activeNetworkInfo
+        val isConnected = info != null && info.isConnected
+        val isValidated = isActiveNetworkValidated
+        val isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(this)
+        val isNotRoaming = info != null && !info.isRoaming
+        return NetworkState(isConnected, isValidated, isMetered, isNotRoaming)
+    } // b/163342798
+
+internal class NetworkStateTrackerPre24(context: Context, taskExecutor: TaskExecutor) :
+    BroadcastReceiverConstraintTracker<NetworkState>(context, taskExecutor) {
+
+    private val connectivityManager: ConnectivityManager =
+        appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+    override fun onBroadcastReceive(intent: Intent) {
+        @Suppress("DEPRECATION")
+        if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
+            Logger.get().debug(TAG, "Network broadcast received")
+            state = connectivityManager.activeNetworkState
+        }
+    }
+
+    @Suppress("DEPRECATION")
+    override val intentFilter: IntentFilter
+        get() = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
+    override val initialState: NetworkState
+        get() = connectivityManager.activeNetworkState
+}
+
+@RequiresApi(24)
+internal class NetworkStateTracker24(context: Context, taskExecutor: TaskExecutor) :
+    ConstraintTracker<NetworkState>(context, taskExecutor) {
+
+    private val connectivityManager: ConnectivityManager =
+        appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+    override val initialState: NetworkState
+        get() = connectivityManager.activeNetworkState
+
+    private val networkCallback = object : NetworkCallback() {
+        override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
+            // The Network parameter is unreliable when a VPN app is running - use active network.
+            Logger.get().debug(TAG, "Network capabilities changed: $capabilities")
+            state = connectivityManager.activeNetworkState
+        }
+        override fun onLost(network: Network) {
+            Logger.get().debug(TAG, "Network connection lost")
+            state = connectivityManager.activeNetworkState
+        }
+    }
+
+    override fun startTracking() {
+        try {
+            Logger.get().debug(TAG, "Registering network callback")
+            connectivityManager.registerDefaultNetworkCallbackCompat(networkCallback)
+        } catch (e: IllegalArgumentException) {
+            // Catching the exceptions since and moving on - this tracker is only used for
+            // GreedyScheduler and there is nothing to be done about device-specific bugs.
+            // IllegalStateException: Happening on NVIDIA Shield K1 Tablets.  See b/136569342.
+            // SecurityException: Happening on Solone W1450.  See b/153246136.
+            Logger.get().error(TAG, "Received exception while registering network callback", e)
+        } catch (e: SecurityException) {
+            Logger.get().error(TAG, "Received exception while registering network callback", e)
+        }
+    }
+
+    override fun stopTracking() {
+        try {
+            Logger.get().debug(TAG, "Unregistering network callback")
+            connectivityManager.unregisterNetworkCallbackCompat(networkCallback)
+        } catch (e: IllegalArgumentException) {
+            // Catching the exceptions since and moving on - this tracker is only used for
+            // GreedyScheduler and there is nothing to be done about device-specific bugs.
+            // IllegalStateException: Happening on NVIDIA Shield K1 Tablets.  See b/136569342.
+            // SecurityException: Happening on Solone W1450.  See b/153246136.
+            Logger.get().error(TAG, "Received exception while unregistering network callback", e)
+        } catch (e: SecurityException) {
+            Logger.get().error(TAG, "Received exception while unregistering network callback", e)
+        }
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
deleted file mode 100644
index 4eb98d1..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Logger;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * Tracks whether or not the device's storage is low.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class StorageNotLowTracker extends BroadcastReceiverConstraintTracker<Boolean> {
-
-    private static final String TAG = Logger.tagWithPrefix("StorageNotLowTracker");
-
-    /**
-     * Create an instance of {@link StorageNotLowTracker}.
-     * @param context The application {@link Context}
-     * @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
-     */
-    public StorageNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
-        super(context, taskExecutor);
-    }
-
-    @Override
-    public Boolean getInitialState() {
-        Intent intent = mAppContext.registerReceiver(null, getIntentFilter());
-        if (intent == null || intent.getAction() == null) {
-            // ACTION_DEVICE_STORAGE_LOW is a sticky broadcast that is removed when sufficient
-            // storage is available again.  ACTION_DEVICE_STORAGE_OK is not sticky.  So if we
-            // don't receive anything here, we can assume that the storage state is okay.
-            return true;
-        } else {
-            switch (intent.getAction()) {
-                case Intent.ACTION_DEVICE_STORAGE_OK:
-                    return true;
-
-                case Intent.ACTION_DEVICE_STORAGE_LOW:
-                    return false;
-
-                default:
-                    // This should never happen because the intent filter is configured
-                    // correctly.
-                    return false;
-            }
-        }
-    }
-
-    @Override
-    public IntentFilter getIntentFilter() {
-        // In API 26+, DEVICE_STORAGE_OK/LOW are deprecated and are no longer sent to
-        // manifest-defined BroadcastReceivers. Since we are registering our receiver manually, this
-        // is currently okay. This may change in future versions, so this will need to be monitored.
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
-        intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
-        return intentFilter;
-    }
-
-    @Override
-    public void onBroadcastReceive(Context context, @NonNull Intent intent) {
-        if (intent.getAction() == null) {
-            return; // Should never happen since the IntentFilter was configured.
-        }
-
-        Logger.get().debug(TAG, "Received " + intent.getAction());
-
-        switch (intent.getAction()) {
-            case Intent.ACTION_DEVICE_STORAGE_OK:
-                setState(true);
-                break;
-
-            case Intent.ACTION_DEVICE_STORAGE_LOW:
-                setState(false);
-                break;
-        }
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.kt
new file mode 100644
index 0000000..815a58a
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/StorageNotLowTracker.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * Tracks whether or not the device's storage is low.
+ * @hide
+ */
+@Suppress("DEPRECATION")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class StorageNotLowTracker(context: Context, taskExecutor: TaskExecutor) :
+    BroadcastReceiverConstraintTracker<Boolean>(context, taskExecutor) {
+
+    override val initialState: Boolean
+        get() {
+            val intent = appContext.registerReceiver(null, intentFilter)
+            return if (intent == null || intent.action == null) {
+                // ACTION_DEVICE_STORAGE_LOW is a sticky broadcast that is removed when sufficient
+                // storage is available again. ACTION_DEVICE_STORAGE_OK is not sticky. So if we
+                // don't receive anything here, we can assume that the storage state is okay.
+                true
+            } else {
+                when (intent.action) {
+                    Intent.ACTION_DEVICE_STORAGE_OK -> true
+                    Intent.ACTION_DEVICE_STORAGE_LOW -> false
+                    else ->
+                        // This should never happen because the intent filter is configured
+                        // correctly.
+                        false
+                }
+            }
+        }
+
+    override val intentFilter: IntentFilter
+        get() {
+            // In API 26+, DEVICE_STORAGE_OK/LOW are deprecated and are no longer sent to
+            // manifest-defined BroadcastReceivers. Since we are registering our receiver manually, this
+            // is currently okay. This may change in future versions, so this will need to be monitored.
+            val intentFilter = IntentFilter()
+            intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK)
+            intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW)
+            return intentFilter
+        }
+
+    override fun onBroadcastReceive(intent: Intent) {
+        if (intent.action == null) {
+            return // Should never happen since the IntentFilter was configured.
+        }
+        Logger.get().debug(TAG, "Received " + intent.action)
+        when (intent.action) {
+            Intent.ACTION_DEVICE_STORAGE_OK -> state = true
+            Intent.ACTION_DEVICE_STORAGE_LOW -> state = false
+        }
+    }
+}
+
+private val TAG = Logger.tagWithPrefix("StorageNotLowTracker")
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
deleted file mode 100644
index ea7fb22..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.trackers;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-
-/**
- * A singleton class to hold an instance of each {@link ConstraintTracker}.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class Trackers {
-    private BatteryChargingTracker mBatteryChargingTracker;
-    private BatteryNotLowTracker mBatteryNotLowTracker;
-    private NetworkStateTracker mNetworkStateTracker;
-    private StorageNotLowTracker mStorageNotLowTracker;
-
-    public Trackers(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
-        Context appContext = context.getApplicationContext();
-        mBatteryChargingTracker = new BatteryChargingTracker(appContext, taskExecutor);
-        mBatteryNotLowTracker = new BatteryNotLowTracker(appContext, taskExecutor);
-        mNetworkStateTracker = new NetworkStateTracker(appContext, taskExecutor);
-        mStorageNotLowTracker = new StorageNotLowTracker(appContext, taskExecutor);
-    }
-
-    /**
-     * Gets the tracker used to track the battery charging status.
-     *
-     * @return The tracker used to track battery charging status
-     */
-    @NonNull
-    public BatteryChargingTracker getBatteryChargingTracker() {
-        return mBatteryChargingTracker;
-    }
-
-    /**
-     * Gets the tracker used to track if the battery is okay or low.
-     *
-     * @return The tracker used to track if the battery is okay or low
-     */
-    @NonNull
-    public BatteryNotLowTracker getBatteryNotLowTracker() {
-        return mBatteryNotLowTracker;
-    }
-
-    /**
-     * Gets the tracker used to track network state changes.
-     *
-     * @return The tracker used to track state of the network
-     */
-    @NonNull
-    public NetworkStateTracker getNetworkStateTracker() {
-        return mNetworkStateTracker;
-    }
-
-    /**
-     * Gets the tracker used to track if device storage is okay or low.
-     *
-     * @return The tracker used to track if device storage is okay or low.
-     */
-    @NonNull
-    public StorageNotLowTracker getStorageNotLowTracker() {
-        return mStorageNotLowTracker;
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.kt
new file mode 100644
index 0000000..d6f8201
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/Trackers.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.trackers
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.work.impl.constraints.NetworkState
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+/**
+ * A singleton class to hold an instance of each [ConstraintTracker].
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class Trackers
+@JvmOverloads
+constructor(
+    context: Context,
+    taskExecutor: TaskExecutor,
+    /**
+     * The tracker used to track the battery charging status.
+     */
+    val batteryChargingTracker: ConstraintTracker<Boolean> =
+        BatteryChargingTracker(context.applicationContext, taskExecutor),
+
+    /**
+     * The tracker used to track if the battery is okay or low.
+     */
+    val batteryNotLowTracker: BatteryNotLowTracker =
+        BatteryNotLowTracker(context.applicationContext, taskExecutor),
+
+    /**
+     * The tracker used to track network state changes.
+     */
+    val networkStateTracker: ConstraintTracker<NetworkState> =
+        NetworkStateTracker(context.applicationContext, taskExecutor),
+
+    /**
+     * The tracker used to track if device storage is okay or low.
+     */
+    val storageNotLowTracker: ConstraintTracker<Boolean> =
+        StorageNotLowTracker(context.applicationContext, taskExecutor),
+)
\ No newline at end of file