Merge "Desktop: fix FPS drop and memory leak" into androidx-master-dev
diff --git a/biometric/biometric/api/1.1.0-alpha03.txt b/biometric/biometric/api/1.1.0-alpha03.txt
new file mode 100644
index 0000000..4a73a63
--- /dev/null
+++ b/biometric/biometric/api/1.1.0-alpha03.txt
@@ -0,0 +1,96 @@
+// Signature format: 3.0
+package androidx.biometric {
+
+  public class BiometricManager {
+    method @Deprecated public int canAuthenticate();
+    method public int canAuthenticate(int);
+    method public static androidx.biometric.BiometricManager from(android.content.Context);
+    field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+    field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+    field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+    field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+  }
+
+  public static interface BiometricManager.Authenticators {
+    field public static final int BIOMETRIC_STRONG = 15; // 0xf
+    field public static final int BIOMETRIC_WEAK = 255; // 0xff
+    field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+  }
+
+  public class BiometricPrompt {
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+    method public void cancelAuthentication();
+    field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+    field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+    field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int ERROR_CANCELED = 5; // 0x5
+    field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_LOCKOUT = 7; // 0x7
+    field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+    field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+    field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+    field public static final int ERROR_NO_SPACE = 4; // 0x4
+    field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int ERROR_VENDOR = 8; // 0x8
+  }
+
+  public abstract static class BiometricPrompt.AuthenticationCallback {
+    ctor public BiometricPrompt.AuthenticationCallback();
+    method public void onAuthenticationError(int, CharSequence);
+    method public void onAuthenticationFailed();
+    method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+  }
+
+  public static class BiometricPrompt.AuthenticationResult {
+    method public int getAuthenticationType();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+  }
+
+  public static class BiometricPrompt.CryptoObject {
+    ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+    method public javax.crypto.Cipher? getCipher();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+    method public javax.crypto.Mac? getMac();
+    method public java.security.Signature? getSignature();
+  }
+
+  public static class BiometricPrompt.PromptInfo {
+    method public int getAllowedAuthenticators();
+    method public CharSequence? getDescription();
+    method public CharSequence getNegativeButtonText();
+    method public CharSequence? getSubtitle();
+    method public CharSequence getTitle();
+    method public boolean isConfirmationRequired();
+    method @Deprecated public boolean isDeviceCredentialAllowed();
+  }
+
+  public static class BiometricPrompt.PromptInfo.Builder {
+    ctor public BiometricPrompt.PromptInfo.Builder();
+    method public androidx.biometric.BiometricPrompt.PromptInfo build();
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+    method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+  }
+
+}
+
diff --git a/biometric/biometric/api/public_plus_experimental_1.1.0-alpha03.txt b/biometric/biometric/api/public_plus_experimental_1.1.0-alpha03.txt
new file mode 100644
index 0000000..4a73a63
--- /dev/null
+++ b/biometric/biometric/api/public_plus_experimental_1.1.0-alpha03.txt
@@ -0,0 +1,96 @@
+// Signature format: 3.0
+package androidx.biometric {
+
+  public class BiometricManager {
+    method @Deprecated public int canAuthenticate();
+    method public int canAuthenticate(int);
+    method public static androidx.biometric.BiometricManager from(android.content.Context);
+    field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+    field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+    field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+    field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+  }
+
+  public static interface BiometricManager.Authenticators {
+    field public static final int BIOMETRIC_STRONG = 15; // 0xf
+    field public static final int BIOMETRIC_WEAK = 255; // 0xff
+    field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+  }
+
+  public class BiometricPrompt {
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+    method public void cancelAuthentication();
+    field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+    field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+    field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int ERROR_CANCELED = 5; // 0x5
+    field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_LOCKOUT = 7; // 0x7
+    field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+    field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+    field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+    field public static final int ERROR_NO_SPACE = 4; // 0x4
+    field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int ERROR_VENDOR = 8; // 0x8
+  }
+
+  public abstract static class BiometricPrompt.AuthenticationCallback {
+    ctor public BiometricPrompt.AuthenticationCallback();
+    method public void onAuthenticationError(int, CharSequence);
+    method public void onAuthenticationFailed();
+    method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+  }
+
+  public static class BiometricPrompt.AuthenticationResult {
+    method public int getAuthenticationType();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+  }
+
+  public static class BiometricPrompt.CryptoObject {
+    ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+    method public javax.crypto.Cipher? getCipher();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+    method public javax.crypto.Mac? getMac();
+    method public java.security.Signature? getSignature();
+  }
+
+  public static class BiometricPrompt.PromptInfo {
+    method public int getAllowedAuthenticators();
+    method public CharSequence? getDescription();
+    method public CharSequence getNegativeButtonText();
+    method public CharSequence? getSubtitle();
+    method public CharSequence getTitle();
+    method public boolean isConfirmationRequired();
+    method @Deprecated public boolean isDeviceCredentialAllowed();
+  }
+
+  public static class BiometricPrompt.PromptInfo.Builder {
+    ctor public BiometricPrompt.PromptInfo.Builder();
+    method public androidx.biometric.BiometricPrompt.PromptInfo build();
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+    method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+  }
+
+}
+
diff --git a/biometric/biometric/api/res-1.1.0-alpha03.txt b/biometric/biometric/api/res-1.1.0-alpha03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/biometric/biometric/api/res-1.1.0-alpha03.txt
diff --git a/biometric/biometric/api/restricted_1.1.0-alpha03.txt b/biometric/biometric/api/restricted_1.1.0-alpha03.txt
new file mode 100644
index 0000000..4a73a63
--- /dev/null
+++ b/biometric/biometric/api/restricted_1.1.0-alpha03.txt
@@ -0,0 +1,96 @@
+// Signature format: 3.0
+package androidx.biometric {
+
+  public class BiometricManager {
+    method @Deprecated public int canAuthenticate();
+    method public int canAuthenticate(int);
+    method public static androidx.biometric.BiometricManager from(android.content.Context);
+    field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
+    field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int BIOMETRIC_ERROR_UNSUPPORTED = -2; // 0xfffffffe
+    field public static final int BIOMETRIC_STATUS_UNKNOWN = -1; // 0xffffffff
+    field public static final int BIOMETRIC_SUCCESS = 0; // 0x0
+  }
+
+  public static interface BiometricManager.Authenticators {
+    field public static final int BIOMETRIC_STRONG = 15; // 0xf
+    field public static final int BIOMETRIC_WEAK = 255; // 0xff
+    field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+  }
+
+  public class BiometricPrompt {
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.FragmentActivity, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    ctor public BiometricPrompt(androidx.fragment.app.Fragment, java.util.concurrent.Executor, androidx.biometric.BiometricPrompt.AuthenticationCallback);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo, androidx.biometric.BiometricPrompt.CryptoObject);
+    method public void authenticate(androidx.biometric.BiometricPrompt.PromptInfo);
+    method public void cancelAuthentication();
+    field public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2; // 0x2
+    field public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1; // 0x1
+    field public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int ERROR_CANCELED = 5; // 0x5
+    field public static final int ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_LOCKOUT = 7; // 0x7
+    field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
+    field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
+    field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
+    field public static final int ERROR_NO_SPACE = 4; // 0x4
+    field public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
+    field public static final int ERROR_TIMEOUT = 3; // 0x3
+    field public static final int ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int ERROR_VENDOR = 8; // 0x8
+  }
+
+  public abstract static class BiometricPrompt.AuthenticationCallback {
+    ctor public BiometricPrompt.AuthenticationCallback();
+    method public void onAuthenticationError(int, CharSequence);
+    method public void onAuthenticationFailed();
+    method public void onAuthenticationSucceeded(androidx.biometric.BiometricPrompt.AuthenticationResult);
+  }
+
+  public static class BiometricPrompt.AuthenticationResult {
+    method public int getAuthenticationType();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+  }
+
+  public static class BiometricPrompt.CryptoObject {
+    ctor public BiometricPrompt.CryptoObject(java.security.Signature);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
+    ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public BiometricPrompt.CryptoObject(android.security.identity.IdentityCredential);
+    method public javax.crypto.Cipher? getCipher();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
+    method public javax.crypto.Mac? getMac();
+    method public java.security.Signature? getSignature();
+  }
+
+  public static class BiometricPrompt.PromptInfo {
+    method public int getAllowedAuthenticators();
+    method public CharSequence? getDescription();
+    method public CharSequence getNegativeButtonText();
+    method public CharSequence? getSubtitle();
+    method public CharSequence getTitle();
+    method public boolean isConfirmationRequired();
+    method @Deprecated public boolean isDeviceCredentialAllowed();
+  }
+
+  public static class BiometricPrompt.PromptInfo.Builder {
+    ctor public BiometricPrompt.PromptInfo.Builder();
+    method public androidx.biometric.BiometricPrompt.PromptInfo build();
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
+    method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
+  }
+
+}
+
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index f319223..657db32 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -20,7 +20,7 @@
 
 // NOTE: lint versions *must* be kept in sync with agp
 if (isUiProject) {
-    build_versions.kotlin = "1.4.0-rc"
+    build_versions.kotlin = "1.4.0"
     build_versions.kotlin_coroutines = "1.3.6"
     build_versions.agp = '4.2.0-alpha06'
     build_versions.lint = '27.2.0-alpha06'
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 7439c33..a8664e3 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -32,7 +32,7 @@
     val ASYNCLAYOUTINFLATER = Version("1.1.0-alpha01")
     val AUTOFILL = Version("1.1.0-alpha02")
     val BENCHMARK = Version("1.1.0-alpha02")
-    val BIOMETRIC = Version("1.1.0-alpha02")
+    val BIOMETRIC = Version("1.1.0-alpha03")
     val BROWSER = Version("1.3.0-alpha05")
     val BUILDSRC_TESTS = Version("1.0.0-alpha01")
     val CAMERA = Version("1.0.0-beta08")
@@ -42,7 +42,7 @@
     val CARDVIEW = Version("1.1.0-alpha01")
     val COLLECTION = Version("1.2.0-alpha01")
     val CONTENTPAGER = Version("1.1.0-alpha01")
-    val COMPOSE = Version("0.1.0-dev17")
+    val COMPOSE = Version("1.0.0-alpha02")
     val CONTENTACCESS = Version("1.0.0-alpha01")
     val COORDINATORLAYOUT = Version("1.2.0-alpha01")
     val CORE = Version("1.5.0-alpha02")
@@ -80,7 +80,7 @@
     val MEDIA2 = Version("1.1.0-alpha02")
     val MEDIAROUTER = Version("1.2.0-alpha02")
     val NAVIGATION = Version("2.4.0-alpha01")
-    val PAGING = Version("3.0.0-alpha05")
+    val PAGING = Version("3.0.0-alpha06")
     val PALETTE = Version("1.1.0-alpha01")
     val PRINT = Version("1.1.0-alpha01")
     val PERCENTLAYOUT = Version("1.1.0-alpha01")
@@ -110,7 +110,7 @@
     val TRACING = Version("1.0.0-beta01")
     val TRANSITION = Version("1.4.0-beta01")
     val TVPROVIDER = Version("1.1.0-alpha01")
-    val UI = Version("0.1.0-dev17")
+    val UI = Version("1.0.0-alpha02")
     val VECTORDRAWABLE = Version("1.2.0-alpha02")
     val VECTORDRAWABLE_ANIMATED = Version("1.2.0-alpha01")
     val VECTORDRAWABLE_SEEKABLE = Version("1.0.0-alpha02")
diff --git a/camera/camera-camera2-pipe/build.gradle b/camera/camera-camera2-pipe/build.gradle
index 346b3bc..9e3e806 100644
--- a/camera/camera-camera2-pipe/build.gradle
+++ b/camera/camera-camera2-pipe/build.gradle
@@ -52,6 +52,7 @@
     testImplementation(JUNIT)
     testImplementation(TRUTH)
     testImplementation(ROBOLECTRIC)
+    testImplementation(KOTLIN_COROUTINES_TEST)
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index dd27f76..f9bcabe 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -241,20 +241,18 @@
 /**
  * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
  */
-fun CaptureRequest.Builder.writeParameters(
-    parameters: Map<*, Any>
-) {
+fun CaptureRequest.Builder.writeParameters(parameters: Map<*, Any>) {
     for ((key, value) in parameters) {
-        if (key is CaptureRequest.Key<*>) {
-            @Suppress("UNCHECKED_CAST")
-            this.writeParameter(key as CaptureRequest.Key<Any>, value)
-        }
+        writeParameter(key, value)
     }
 }
 
 /**
  * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
  */
-fun <T> CaptureRequest.Builder.writeParameter(key: CaptureRequest.Key<T>, value: T) {
-    this.set(key, value)
+fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any?) {
+    if (key != null && key is CaptureRequest.Key<*>) {
+        @Suppress("UNCHECKED_CAST")
+        this.set(key as CaptureRequest.Key<Any>, value)
+    }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
index 6612e5e..c1385e1 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
@@ -16,18 +16,14 @@
 
 package androidx.camera.camera2.pipe.impl
 
-import android.os.Process
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.Request
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import dagger.Subcomponent
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.asCoroutineDispatcher
-import java.util.concurrent.Executors
 import javax.inject.Qualifier
 import javax.inject.Scope
 
@@ -73,31 +69,8 @@
     @CameraGraphScope
     @Provides
     @ForCameraGraph
-    fun provideCameraGraphCoroutineScope(
-        @ForCameraGraph dispatcher: CoroutineDispatcher
-    ): CoroutineScope {
-        return CoroutineScope(dispatcher.plus(CoroutineName("CXCP-Graph")))
-    }
-
-    @CameraGraphScope
-    @Provides
-    @ForCameraGraph
-    fun provideCameraGraphCoroutineDispatcher(): CoroutineDispatcher {
-        // TODO: Figure out how to make sure the dispatcher gets shut down.
-        return Executors.newFixedThreadPool(1) {
-            object : Thread(it) {
-                init {
-                    name = "CXCP-Graph"
-                }
-
-                override fun run() {
-                    Process.setThreadPriority(
-                        Process.THREAD_PRIORITY_DISPLAY + Process.THREAD_PRIORITY_LESS_FAVORABLE
-                    )
-                    super.run()
-                }
-            }
-        }.asCoroutineDispatcher()
+    fun provideCameraGraphCoroutineScope(threads: Threads): CoroutineScope {
+        return CoroutineScope(threads.defaultDispatcher.plus(CoroutineName("CXCP-Graph")))
     }
 
     @CameraGraphScope
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
index 1ba7495..9860d60 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
@@ -31,6 +31,7 @@
     private val graphProcessor: GraphProcessor,
     private val streamMap: StreamMap
 ) : CameraGraph {
+    private val debugId = Debug.debugIdsForGraph.incrementAndGet()
     // Only one session can be active at a time.
     private val sessionLock = TokenLockImpl(1)
     override val streams: Map<StreamConfig, Stream>
@@ -62,4 +63,6 @@
         sessionLock.close()
         graphProcessor.close()
     }
+
+    override fun toString(): String = "CameraGraph-$debugId"
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
index 9876f82..0326dcb 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
@@ -23,6 +23,7 @@
     private val token: TokenLock.Token,
     private val graphProcessor: GraphProcessor
 ) : CameraGraph.Session {
+    private val debugId = Debug.debugIdsForGraphSession.incrementAndGet()
     override fun submit(request: Request) {
         graphProcessor.submit(request)
     }
@@ -43,4 +44,5 @@
         // Release the token so that a new instance of session can be created.
         token.release()
     }
+    override fun toString(): String = "CameraGraph.Session-$debugId"
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
index 40cd98b..af67e59 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
@@ -16,17 +16,12 @@
 
 package androidx.camera.camera2.pipe.impl
 
-import android.Manifest.permission
 import android.content.Context
-import android.content.pm.PackageManager
 import android.hardware.camera2.CameraManager
-import android.os.Build
 import android.util.ArrayMap
 import androidx.annotation.GuardedBy
-import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -39,14 +34,13 @@
  */
 @Singleton
 class CameraMetadataCache @Inject constructor(
-    private val context: Context
+    private val context: Context,
+    private val threads: Threads,
+    private val permissions: Permissions
 ) {
     @GuardedBy("cache")
     private val cache = ArrayMap<String, CameraMetadata>()
 
-    @Volatile
-    private var hasCameraPermission = false
-
     suspend fun get(cameraId: CameraId): CameraMetadata {
         synchronized(cache) {
             val existing = cache[cameraId.value]
@@ -56,7 +50,7 @@
         }
 
         // Suspend and query CameraMetadata on a background thread.
-        return withContext(Dispatchers.IO) {
+        return withContext(threads.ioDispatcher) {
             awaitMetadata(cameraId)
         }
     }
@@ -85,26 +79,5 @@
         return CameraMetadataImpl(cameraId, redacted, characteristics, emptyMap())
     }
 
-    private fun isMetadataRedacted(): Boolean {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            // Some CameraCharacteristic properties are redacted on Q or higher if the application
-            // does not currently hold the CAMERA permission.
-            return !checkCameraPermission()
-        }
-        return false
-    }
-
-    @RequiresApi(23)
-    private fun checkCameraPermission(): Boolean {
-        // Granted camera permission is cached here to reduce the number of binder transactions
-        // executed.  This is considered okay because when a user revokes a permission at runtime,
-        // Android's PermissionManagerService kills the app via the onPermissionRevoked callback,
-        // allowing the code to avoid re-querying after checkSelfPermission returns true.
-        if (!hasCameraPermission &&
-            context.checkSelfPermission(permission.CAMERA) == PackageManager.PERMISSION_GRANTED
-        ) {
-            hasCameraPermission = true
-        }
-        return hasCameraPermission
-    }
+    private fun isMetadataRedacted(): Boolean = !permissions.hasCameraPermission
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
index 3cd222f..665fe0a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import android.hardware.camera2.CameraManager
+import android.os.Handler
+import android.os.Process
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.Cameras
 import dagger.Binds
@@ -25,8 +27,17 @@
 import dagger.Module
 import dagger.Provides
 import dagger.Reusable
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import java.util.concurrent.Executors
+import javax.inject.Qualifier
 import javax.inject.Singleton
 
+@Qualifier
+annotation class ForCameraPipe
+
 @Singleton
 @Component(modules = [CameraPipeModule::class])
 interface CameraPipeComponent {
@@ -56,5 +67,58 @@
         @Provides
         fun provideCameraManager(context: Context): CameraManager =
             context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+        @Singleton
+        @Provides
+        fun provideCameraPipeThreads(config: CameraPipe.Config): Threads {
+
+            val threadIds = atomic(0)
+            val cameraExecutor = Executors.newFixedThreadPool(2) {
+                object : Thread(it) {
+                    init {
+                        val number = threadIds.incrementAndGet().toString().padStart(2, '0')
+                        name = "CXCP-$number"
+                    }
+
+                    override fun run() {
+                        Process.setThreadPriority(
+                            Process.THREAD_PRIORITY_DISPLAY + Process.THREAD_PRIORITY_LESS_FAVORABLE
+                        )
+                        super.run()
+                    }
+                }
+            }
+            val cameraDispatcher = cameraExecutor.asCoroutineDispatcher()
+            val cameraHandlerProvider =
+                {
+                    @Suppress("DEPRECATION")
+                    config.cameraThread?.let { Handler(it.looper) } ?: Handler()
+                }
+            val ioExecutor = Executors.newFixedThreadPool(8) {
+                object : Thread(it) {
+                    init {
+                        val number = threadIds.incrementAndGet().toString().padStart(2, '0')
+                        name = "CXCP-IO-$number"
+                    }
+                }
+            }
+            val ioDispatcher = ioExecutor.asCoroutineDispatcher()
+
+            val globalScope = CoroutineScope(
+                cameraDispatcher.plus(
+                    CoroutineName
+                        ("CXCP-Pipe")
+                )
+            )
+
+            return Threads(
+                globalScope = globalScope,
+                defaultExecutor = cameraExecutor,
+                defaultDispatcher = cameraDispatcher,
+                ioExecutor = ioExecutor,
+                ioDispatcher = ioDispatcher,
+                handlerBuilder = cameraHandlerProvider
+            )
+        }
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
index 9039e58..284b984 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
@@ -20,6 +20,7 @@
 
 import android.os.Trace
 import android.os.Build
+import kotlinx.atomicfu.atomic
 
 /**
  * Internal debug utilities, constants, and checks.
@@ -67,6 +68,11 @@
             throw IllegalArgumentException(msg())
         }
     }
+
+    internal val debugIdsForGraph = atomic(0)
+    internal val debugIdsForGraphSession = atomic(0)
+    internal val debugIdsForCameraCallback = atomic(0)
+    internal val debugIdsForVirtualCamera = atomic(0)
 }
 
 /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessorImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessorImpl.kt
index 8304760..f04d9da 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessorImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessorImpl.kt
@@ -20,7 +20,6 @@
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.impl.Log.warn
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -70,8 +69,8 @@
  */
 @CameraGraphScope
 class GraphProcessorImpl @Inject constructor(
+    private val threads: Threads,
     @ForCameraGraph private val graphScope: CoroutineScope,
-    @ForCameraGraph private val graphDispatcher: CoroutineDispatcher,
     @ForCameraGraph private val graphListeners: java.util.ArrayList<Request.Listener>
 ) : GraphProcessor {
     private val lock = Any()
@@ -228,7 +227,7 @@
      * Submit a request to the camera using only the current repeating request.
      */
     suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean =
-        withContext(graphDispatcher) {
+        withContext(threads.ioDispatcher) {
             val processor: RequestProcessor?
             val request: Request?
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Metrics.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Metrics.kt
new file mode 100644
index 0000000..ab5e7f1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Metrics.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.camera.camera2.pipe.impl
+
+import android.os.SystemClock
+
+object Metrics {
+    inline fun monotonicNanos(): Long = SystemClock.elapsedRealtimeNanos()
+    inline fun monotonicMillis(): Long = SystemClock.elapsedRealtime()
+    inline fun nanosToMillis(duration: Long): Long = duration / 1_000_000
+    inline fun nanosToMillisDouble(duration: Long): Double = duration.toDouble() / 1_000_000.0
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
new file mode 100644
index 0000000..de0cead
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.pipe.impl
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.os.Build
+import androidx.annotation.RequiresApi
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * This tracks internal permission requests to avoid querying multiple times.
+ *
+ * This class assumes that permissions are one way - They can be granted, but not un-granted
+ * without restarting the application process.
+ */
+@Singleton
+class Permissions @Inject constructor(private val context: Context) {
+    @Volatile
+    private var _hasCameraPermission = false
+    val hasCameraPermission: Boolean
+        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            checkCameraPermission()
+        } else {
+            // On older versions of Android, permissions are required in order to install a package
+            // and so the permission check is redundant.
+            true
+        }
+
+    @RequiresApi(23)
+    private fun checkCameraPermission(): Boolean {
+        // Granted camera permission is cached here to reduce the number of binder transactions
+        // executed.  This is considered okay because when a user revokes a permission at runtime,
+        // Android's PermissionManagerService kills the app via the onPermissionRevoked callback,
+        // allowing the code to avoid re-querying after checkSelfPermission returns true.
+        if (!_hasCameraPermission &&
+            context.checkSelfPermission(Manifest.permission.CAMERA) == PERMISSION_GRANTED
+        ) {
+            _hasCameraPermission = true
+        }
+        return _hasCameraPermission
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
index 15fbf62..8df0b14 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
@@ -22,7 +22,6 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.TotalCaptureResult
-import android.os.Handler
 import android.util.ArrayMap
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraph
@@ -146,7 +145,7 @@
 class StandardRequestProcessor(
     private val device: CameraDeviceWrapper,
     private val session: CameraCaptureSessionWrapper,
-    private val handler: Handler?,
+    private val threads: Threads,
     private val graphConfig: CameraGraph.Config,
     private val streamMap: StreamMap,
     private val graphListeners: List<Request.Listener>
@@ -369,17 +368,26 @@
         // behavior on the CaptureSequence listener have been designed to minimize the number of
         // synchronized calls.
         synchronized(lock = captureSequence) {
+            // TODO: Update these calls to use executors on newer versions of the OS
             val sequenceNumber: Int = if (captureRequests.size == 1) {
                 if (isRepeating) {
-                    session.setRepeatingRequest(captureRequests[0], captureSequence, handler)
+                    session.setRepeatingRequest(
+                        captureRequests[0],
+                        captureSequence,
+                        threads.defaultHandler
+                    )
                 } else {
-                    session.capture(captureRequests[0], captureSequence, handler)
+                    session.capture(captureRequests[0], captureSequence, threads.defaultHandler)
                 }
             } else {
                 if (isRepeating) {
-                    session.setRepeatingBurst(captureRequests, captureSequence, handler)
+                    session.setRepeatingBurst(
+                        captureRequests,
+                        captureSequence,
+                        threads.defaultHandler
+                    )
                 } else {
-                    session.captureBurst(captureRequests, captureSequence, handler)
+                    session.captureBurst(captureRequests, captureSequence, threads.defaultHandler)
                 }
             }
             captureSequence.setSequenceId(SequenceNumber(sequenceNumber))
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Threads.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Threads.kt
new file mode 100644
index 0000000..5e90603
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Threads.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.pipe.impl
+
+import android.os.Handler
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import java.util.concurrent.Executor
+
+class Threads(
+    val globalScope: CoroutineScope,
+
+    val defaultExecutor: Executor,
+    val defaultDispatcher: CoroutineDispatcher,
+
+    val ioExecutor: Executor,
+    val ioDispatcher: CoroutineDispatcher,
+
+    private val handlerBuilder: () -> Handler
+) {
+    private val _defaultHandler = lazy { handlerBuilder() }
+    val defaultHandler: Handler
+        get() = _defaultHandler.value
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Token.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Token.kt
new file mode 100644
index 0000000..d9a0ff0
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Token.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.pipe.impl
+
+/**
+ * A token is used to track access to underlying resources. Implementations must be thread-safe.
+ */
+interface Token {
+    /**
+     * Release this token instance. Return true if this is the first time release has been called
+     * on this token.
+     */
+    fun release(): Boolean
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
new file mode 100644
index 0000000..6de8bd1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
@@ -0,0 +1,353 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("EXPERIMENTAL_API_USAGE")
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraDevice
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
+import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
+import androidx.camera.camera2.pipe.wrapper.closeWithTrace
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+sealed class CameraState
+object CameraStateUnopened : CameraState()
+data class CameraStateOpen(val cameraDevice: CameraDeviceWrapper) : CameraState()
+object CameraStateClosing : CameraState()
+data class CameraStateClosed(
+    val cameraId: CameraId,
+
+    // Record the reason that the camera was closed.
+    val cameraClosedReason: ClosedReason,
+
+    // Record the number of retry attempts, if the camera took multiple attempts to open.
+    val cameraRetryCount: Int? = null,
+
+    // Record the number of nanoseconds it took to open the camera, including retry attempts.
+    val cameraRetryDurationNs: Long? = null,
+
+    // Record the exception that was thrown while trying to open the camera
+    val cameraException: Throwable? = null,
+
+    // Record the number of nanoseconds it took for the final open attempt.
+    val cameraOpenDurationNs: Long? = null,
+
+    // Record the duration the camera device was active. If onOpened is never called, this value
+    // will never be set.
+    val cameraActiveDurationNs: Long? = null,
+
+    // Record the duration the camera device took to invoke close() on the CameraDevice object.
+    val cameraClosingDurationNs: Long? = null,
+
+    // Record the Camera2 ErrorCode, if the camera closed due to an error.
+    val cameraErrorCode: Int? = null
+) : CameraState()
+
+enum class ClosedReason {
+    APP_CLOSED,
+    APP_DISCONNECTED,
+
+    CAMERA2_CLOSED,
+    CAMERA2_DISCONNECTED,
+    CAMERA2_ERROR,
+    CAMERA2_EXCEPTION
+}
+
+/**
+ * A [VirtualCamera] reflects and replays the state of a "Real" [CameraDevice.StateCallback].
+ *
+ * This behavior allows a virtual camera to be attached a [CameraDevice.StateCallback] and to
+ * replay the open sequence. This behavior a camera manager to run multiple open attempts and to
+ * recover from various classes of errors that will be invisible to the [VirtualCamera] by
+ * allowing the [VirtualCamera] to be attached to the real camera after the camera is opened
+ * successfully (Which may involve multiple calls to open).
+ *
+ * Disconnecting the VirtualCamera will cause an artificial close events to be generated on the
+ * state property, but may not cause the underlying [CameraDevice] to be closed.
+ */
+interface VirtualCamera {
+    val state: Flow<CameraState>
+    fun disconnect()
+}
+
+class VirtualCameraState(
+    val cameraId: CameraId
+) : VirtualCamera {
+    private val debugId = Debug.debugIdsForVirtualCamera.incrementAndGet()
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private var closed = false
+
+    private val _state = MutableStateFlow<CameraState>(CameraStateUnopened)
+    override val state: StateFlow<CameraState>
+        get() = _state
+
+    private var job: Job? = null
+    private var token: Token? = null
+
+    internal suspend fun connect(state: Flow<CameraState>, wakelockToken: Token?) = coroutineScope {
+        synchronized(lock) {
+            if (closed) {
+                wakelockToken?.release()
+                return@coroutineScope
+            }
+
+            job = launch {
+                state.collect { _state.value = it }
+            }
+            token = wakelockToken
+        }
+    }
+
+    override fun disconnect() {
+        synchronized(lock) {
+            if (closed) {
+                return
+            }
+            closed = true
+
+            Log.info { "Disconnecting $this" }
+
+            job?.cancel()
+            token?.release()
+
+            // Emulate a CameraClosing -> CameraClosed sequence.
+            if (_state.value !is CameraStateClosed) {
+                if (_state.value !is CameraStateClosing) {
+                    _state.value = CameraStateClosing
+                }
+                @SuppressWarnings("SyntheticAccessor")
+                _state.value = CameraStateClosed(
+                    cameraId,
+                    cameraClosedReason = ClosedReason.APP_DISCONNECTED
+                )
+            }
+        }
+    }
+
+    override fun toString(): String = "VirtualCamera-$debugId"
+}
+
+internal class AndroidCameraState(
+    val cameraId: CameraId,
+    val metadata: CameraMetadata,
+    private val attemptNumber: Int,
+    private val attemptTimestampNanos: Long
+) : CameraDevice.StateCallback() {
+    private val debugId = Debug.debugIdsForCameraCallback.incrementAndGet()
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private var opening = false
+
+    @GuardedBy("lock")
+    private var pendingClose: ClosingInfo? = null
+
+    private val requestTimestampNanos: Long
+    private var openTimestampNanos: Long? = null
+
+    private val _state = MutableStateFlow<CameraState>(CameraStateUnopened)
+    val state: StateFlow<CameraState>
+        get() = _state
+
+    init {
+        Log.debug { "$cameraId: Opening" }
+        requestTimestampNanos =
+            if (attemptNumber == 1) {
+                attemptTimestampNanos
+            } else {
+                Metrics.monotonicNanos()
+            }
+    }
+
+    fun close() {
+        val current = _state.value
+        val device = if (current is CameraStateOpen) {
+            current.cameraDevice
+        } else {
+            null
+        }
+
+        Log.info { "About to close $device" }
+
+        closeWith(
+            device?.unwrap(),
+            ClosingInfo(ClosedReason.APP_CLOSED)
+        )
+    }
+
+    suspend fun awaitClosed() {
+        state.first { it is CameraStateClosed }
+    }
+
+    override fun onOpened(cameraDevice: CameraDevice) {
+        check(cameraDevice.id == cameraId.value)
+        val openedTimestamp = Metrics.monotonicNanos()
+        openTimestampNanos = openedTimestamp
+        val attemptDuration = Metrics.nanosToMillis(openedTimestamp - requestTimestampNanos)
+        val totalDuration = Metrics.nanosToMillis(openedTimestamp - attemptTimestampNanos)
+        Log.debug {
+            if (attemptNumber == 1) {
+                "$cameraId: onOpened after ${attemptDuration}ms"
+            } else {
+                "$cameraId: onOpened after ${attemptDuration}ms " +
+                        "(${totalDuration}ms total) and $attemptNumber attempts."
+            }
+        }
+
+        // This checks to see if close() has been invoked, or one of the close methods have been
+        // invoked. If so, call close() on the cameraDevice outside of the synchronized block.
+        var closeCamera = false
+        synchronized(lock) {
+            if (pendingClose != null) {
+                closeCamera = true
+            } else {
+                opening = true
+            }
+        }
+        if (closeCamera) {
+            cameraDevice.close()
+            return
+        }
+
+        // Update _state.value _without_ holding the lock. This may block the calling thread for a
+        // while if it synchronously calls createCaptureSession.
+        _state.value = CameraStateOpen(
+            AndroidCameraDevice(
+                metadata,
+                cameraDevice,
+                cameraId
+            )
+        )
+
+        // Check to see if we received close() or other events in the meantime.
+        val closeInfo = synchronized(lock) {
+            opening = false
+            pendingClose
+        }
+        if (closeInfo != null) {
+            _state.value = CameraStateClosing
+            cameraDevice.closeWithTrace()
+            _state.value = computeClosedState(closeInfo)
+        }
+    }
+
+    override fun onDisconnected(cameraDevice: CameraDevice) {
+        check(cameraDevice.id == cameraId.value)
+        Log.debug { "$cameraId: onDisconnected" }
+
+        closeWith(
+            cameraDevice,
+            ClosingInfo(ClosedReason.CAMERA2_DISCONNECTED)
+        )
+    }
+
+    override fun onError(cameraDevice: CameraDevice, errorCode: Int) {
+        check(cameraDevice.id == cameraId.value)
+        Log.debug { "$cameraId: onError $errorCode" }
+
+        closeWith(
+            cameraDevice,
+            ClosingInfo(ClosedReason.CAMERA2_ERROR, errorCode = errorCode)
+        )
+    }
+
+    override fun onClosed(cameraDevice: CameraDevice) {
+        check(cameraDevice.id == cameraId.value)
+        Log.debug { "$cameraId: onClosed" }
+
+        closeWith(cameraDevice, ClosingInfo(ClosedReason.CAMERA2_CLOSED))
+    }
+
+    internal fun closeWith(throwable: Throwable) {
+        closeWith(
+            null,
+            ClosingInfo(
+                ClosedReason.CAMERA2_EXCEPTION,
+                exception = throwable
+            )
+        )
+    }
+
+    private fun closeWith(cameraDevice: CameraDevice?, closeRequest: ClosingInfo) {
+        val closeInfo = synchronized(lock) {
+            if (pendingClose == null) {
+                pendingClose = closeRequest
+                if (!opening) {
+                    return@synchronized closeRequest
+                }
+            }
+            null
+        }
+        if (closeInfo != null) {
+            _state.value = CameraStateClosing
+            cameraDevice.closeWithTrace()
+            _state.value = computeClosedState(closeInfo)
+        }
+    }
+
+    private fun computeClosedState(
+        closingInfo: ClosingInfo
+    ): CameraStateClosed {
+        val now = Metrics.monotonicNanos()
+        val openedTimestamp = openTimestampNanos
+        val closingTimestamp = closingInfo.closingTimestamp
+        val retryDuration = openedTimestamp?.let { it - attemptTimestampNanos }
+        val openDuration = openedTimestamp?.let { it - requestTimestampNanos }
+
+        // opened -> closing (or now)
+        val activeDuration = when {
+            openedTimestamp == null -> null
+            else -> closingTimestamp - openedTimestamp
+        }
+
+        val closeDuration = closingTimestamp.let { now - it }
+
+        @Suppress("SyntheticAccessor")
+        return CameraStateClosed(
+            cameraId,
+            cameraClosedReason = closingInfo.reason,
+            cameraRetryCount = attemptNumber - 1,
+            cameraRetryDurationNs = retryDuration,
+            cameraOpenDurationNs = openDuration,
+            cameraActiveDurationNs = activeDuration,
+            cameraClosingDurationNs = closeDuration,
+            cameraErrorCode = closingInfo.errorCode,
+            cameraException = closingInfo.exception
+        )
+    }
+
+    private data class ClosingInfo(
+        val reason: ClosedReason,
+        val closingTimestamp: Long = Metrics.monotonicNanos(),
+        val errorCode: Int? = null,
+        val exception: Throwable? = null
+    )
+
+    override fun toString(): String = "CameraState-$debugId"
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
new file mode 100644
index 0000000..dd53121
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
@@ -0,0 +1,384 @@
+/*
+ * 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.pipe.impl
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraManager
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraId
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeoutOrNull
+import javax.inject.Inject
+import javax.inject.Provider
+import javax.inject.Singleton
+
+internal sealed class CameraRequest
+internal data class RequestOpen(
+    val virtualCamera: VirtualCameraState,
+    val share: Boolean = false
+) : CameraRequest()
+
+internal data class RequestClose(
+    val activeCamera: VirtualCameraManager.ActiveCamera
+) : CameraRequest()
+
+internal object RequestCloseAll : CameraRequest()
+
+@Suppress("EXPERIMENTAL_API_USAGE")
+@Singleton
+class VirtualCameraManager @Inject constructor(
+    private val cameraManager: Provider<CameraManager>,
+    private val cameraMetadata: CameraMetadataCache,
+    private val permissions: Permissions,
+    private val threads: Threads
+) {
+    private val requestQueue: Channel<CameraRequest> = Channel(8)
+    private val activeCameras: MutableSet<ActiveCamera> = mutableSetOf()
+
+    init {
+        threads.globalScope.launch(CoroutineName("CXCP-VirtualCameraManager")) { requestLoop() }
+    }
+
+    fun open(cameraId: CameraId, share: Boolean = false): VirtualCamera {
+        val result = VirtualCameraState(cameraId)
+        offerChecked(RequestOpen(result, share))
+        return result
+    }
+
+    fun closeAll() {
+        offerChecked(RequestCloseAll)
+    }
+
+    private fun offerChecked(request: CameraRequest) {
+        check(requestQueue.offer(request)) { " There are more than 8 requests buffered!" }
+    }
+
+    private suspend fun requestLoop() = coroutineScope {
+        val requests = arrayListOf<CameraRequest>()
+
+        while (true) {
+            // Stage 1: We have a request, but there is a chance we have received multiple
+            //   requests.
+            readRequestQueue(requests)
+
+            // Prioritize requests that remove specific cameras from the list of active cameras.
+            val closeRequest = requests.firstOrNull { it is RequestClose } as? RequestClose
+            if (closeRequest != null) {
+                requests.remove(closeRequest)
+                Log.info { "CloseRequest: $closeRequest" }
+                if (activeCameras.contains(closeRequest.activeCamera)) {
+                    activeCameras.remove(closeRequest.activeCamera)
+                }
+
+                launch {
+                    Log.info { "Closing active camera" }
+                    closeRequest.activeCamera.close()
+                }
+                Log.info { "Waiting for active camera to close" }
+                closeRequest.activeCamera.awaitClosed()
+                continue
+            }
+
+            // If we received a closeAll request, then close every request leading up to it.
+            val closeAll = requests.indexOfLast { it is RequestCloseAll }
+            if (closeAll >= 0) {
+                for (i in 0..closeAll) {
+                    val request = requests[0]
+                    if (request is RequestOpen) {
+                        request.virtualCamera.disconnect()
+                    }
+                    requests.removeAt(0)
+                }
+
+                // Close all active cameras.
+                for (activeCamera in activeCameras) {
+                    launch {
+                        activeCamera.close()
+                    }
+                }
+                for (camera in activeCameras) {
+                    camera.awaitClosed()
+                }
+                activeCameras.clear()
+                continue
+            }
+
+            // The only way we get to this point is if:
+            // A) We received a request
+            // B) That request was NOT a Close, or CloseAll request
+            val request = requests[0]
+            check(request is RequestOpen)
+
+            // Sanity Check: If the camera we are attempting to open is now closed or disconnected,
+            // skip this virtual camera request.
+            if (request.virtualCamera.state.value !is CameraStateUnopened) {
+                requests.remove(request)
+                continue
+            }
+
+            // Stage 2: Intermediate requests have been discarded, and we need to evaluate the set
+            //   of currently open cameras to the set of desired cameras and close ones that are not
+            //   needed. Since close may block, we will re-evaluate the next request after the
+            //   desired cameras are closed since new requests may have arrived.
+            val cameraIdToOpen = request.virtualCamera.cameraId
+            val camerasToClose = if (request.share) {
+                emptyList()
+            } else {
+                activeCameras.filter { it.cameraId != cameraIdToOpen }
+            }
+
+            if (camerasToClose.isNotEmpty()) {
+                // Shutdown of cameras should always happen first (and suspend until complete)
+                activeCameras.removeAll(camerasToClose)
+                for (camera in camerasToClose) {
+                    // TODO: This should be a dispatcher instead of scope.launch
+
+                    launch {
+                        // TODO: Figure out if this should be blocking or not. If we are directly invoking
+                        //   close this method could block for 0-1000ms
+                        camera.close()
+                    }
+                }
+                for (realCamera in camerasToClose) {
+                    realCamera.awaitClosed()
+                }
+                continue
+            }
+
+            // Stage 3: Open or select an active camera device.
+            var realCamera = activeCameras.firstOrNull { it.cameraId == cameraIdToOpen }
+            if (realCamera == null) {
+                realCamera = openCameraWithRetry(cameraIdToOpen, scope = this)
+                activeCameras.add(realCamera)
+                continue
+            }
+
+            // Stage 4: Attach camera(s)
+            realCamera.connectTo(request.virtualCamera)
+            requests.remove(request)
+        }
+    }
+
+    private suspend fun readRequestQueue(requests: MutableList<CameraRequest>) {
+        if (requests.isEmpty()) {
+            requests.add(requestQueue.receive())
+        }
+
+        // We have a request, but there is a chance we have received multiple requests while we
+        // were doing other things (like opening a camera).
+        while (!requestQueue.isEmpty) {
+            requests.add(requestQueue.receive())
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    private suspend fun openCameraWithRetry(
+        cameraId: CameraId,
+        scope: CoroutineScope
+    ): ActiveCamera {
+        val metadata = cameraMetadata.get(cameraId)
+        val requestTimestamp = Metrics.monotonicNanos()
+
+        var cameraState: AndroidCameraState
+        var attempts = 0
+
+        // TODO: Figure out how 1-time permissions work, and see if they can be reset without
+        //   causing the application process to restart.
+        check(permissions.hasCameraPermission) { "Missing camera permissions!" }
+
+        while (true) {
+            attempts++
+            val instance = cameraManager.get()
+            cameraState = AndroidCameraState(
+                cameraId,
+                metadata,
+                attempts,
+                requestTimestamp
+            )
+
+            var exception: Throwable? = null
+            try {
+                Debug.trace("CameraId ${cameraId.value}#openCamera") {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                        instance.openCamera(
+                            cameraId.value,
+                            threads.defaultExecutor,
+                            cameraState
+                        )
+                    } else {
+                        instance.openCamera(
+                            cameraId.value,
+                            cameraState,
+                            threads.defaultHandler
+                        )
+                    }
+                }
+
+                // Suspend until we are no longer in a "starting" state.
+                val result = cameraState.state.first {
+                    it !is CameraStateUnopened
+                }
+                if (result is CameraStateOpen) {
+                    return ActiveCamera(
+                        cameraState,
+                        scope,
+                        requestQueue
+                    )
+                }
+            } catch (e: Throwable) {
+                exception = e
+                Log.warn(e) { "CameraId ${cameraId.value}: Failed to open" }
+            }
+
+            // TODO: Add logic to optimize retry handling for various error codes and exceptions.
+
+//            var errorCode: Int? = null
+//            if (lastResult is CameraClosed) {
+//                errorCode = lastResult.camera2ErrorCode
+//            }
+//
+//            if (lastException != null) {
+//                when (lastException) {
+//                    is CameraAccessException -> retry = true
+//                    is IllegalArgumentException -> retry = true
+//                    is SecurityException -> {
+//                        if ()
+//                    }
+//                }
+//            }
+
+            if (attempts > 3) {
+                if (exception != null) {
+                    cameraState.closeWith(exception)
+                } else {
+                    cameraState.close()
+                }
+            }
+
+            // Listen to availability - if we are notified that the cameraId is available then
+            // retry immediately.
+            awaitAvailableCameraId(cameraId, timeoutMillis = 500)
+        }
+    }
+
+    /**
+     * Wait for the specified duration, or until the availability callback is invoked.
+     */
+    private suspend fun awaitAvailableCameraId(
+        cameraId: CameraId,
+        timeoutMillis: Long = 200
+    ): Boolean {
+        val manager = cameraManager.get()
+
+        val cameraAvailableEvent = CompletableDeferred<Boolean>()
+
+        val availabilityCallback = object : CameraManager.AvailabilityCallback() {
+            override fun onCameraAvailable(cameraIdString: String) {
+                if (cameraIdString == cameraId.value) {
+                    Log.debug { "$cameraId is now available. Retry." }
+                    cameraAvailableEvent.complete(true)
+                }
+            }
+
+            override fun onCameraAccessPrioritiesChanged() {
+                Log.debug { "Access priorities changed. Retry." }
+                cameraAvailableEvent.complete(true)
+            }
+        }
+
+        // WARNING: Only one registerAvailabilityCallback can be set at a time.
+        // TODO: Turn this into a broadcast service so that multiple listeners can be registered if
+        //  needed.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            manager.registerAvailabilityCallback(threads.defaultExecutor, availabilityCallback)
+        } else {
+            manager.registerAvailabilityCallback(availabilityCallback, threads.defaultHandler)
+        }
+
+        // Suspend until timeout fires or until availability callback fires.
+        val available = withTimeoutOrNull(timeoutMillis) {
+            cameraAvailableEvent.await()
+        } ?: false
+        manager.unregisterAvailabilityCallback(availabilityCallback)
+
+        if (!available) {
+            Log.info { "$cameraId was not available after $timeoutMillis ms!" }
+        }
+
+        return available
+    }
+
+    internal class ActiveCamera(
+        private val androidCameraState: AndroidCameraState,
+        scope: CoroutineScope,
+        channel: SendChannel<CameraRequest>
+    ) {
+        val cameraId: CameraId
+            get() = androidCameraState.cameraId
+
+        private val listenerJob: Job
+        private var current: VirtualCameraState? = null
+
+        private val wakelock = WakeLock(
+            scope,
+            timeout = 1000,
+            callback = {
+                Log.debug { "Wakelock expired." }
+                channel.offer(RequestClose(this))
+            })
+
+        init {
+            listenerJob = scope.launch {
+                androidCameraState.state.collect {
+                    if (it is CameraStateClosing || it is CameraStateClosed) {
+                        Log.debug { " Camera is in $it state, releasing wakelock" }
+                        wakelock.release()
+                        this.cancel()
+                    }
+                }
+            }
+        }
+
+        suspend fun connectTo(virtualCameraState: VirtualCameraState) {
+            val token = wakelock.acquire()
+            val previous = current
+            current = virtualCameraState
+
+            previous?.disconnect()
+            virtualCameraState.connect(androidCameraState.state, token)
+        }
+
+        fun close() {
+            wakelock.release()
+            androidCameraState.close()
+        }
+
+        suspend fun awaitClosed() {
+            androidCameraState.awaitClosed()
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/WakeLock.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/WakeLock.kt
new file mode 100644
index 0000000..0a0b787
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/WakeLock.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.pipe.impl
+
+import androidx.annotation.GuardedBy
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * A wakelock is a thread-safe primitive that can invoke close after all tokens are released.
+ *
+ * This implementation has several defining characteristics:
+ * 1. The timeout (if specified), does not start until at least 1 [Token] has been acquired.
+ * 2. Acquiring a token a token at any time before the timeout completes will cancel the timeout.
+ * 3. Acquiring a token is atomic: Either a token is acquired and the close method will not execute
+ *    OR acquire will return a token and the close method will not execute until after the token is
+ *    released.
+ */
+class WakeLock(
+    private val scope: CoroutineScope,
+    private val timeout: Long = 0,
+    private val callback: () -> Unit
+) {
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private var count = 0
+
+    @GuardedBy("lock")
+    private var timeoutJob: Job? = null
+
+    @GuardedBy("lock")
+    private var closed = false
+
+    private inner class WakeLockToken : Token {
+        private val closed = atomic(false)
+        override fun release(): Boolean {
+            if (closed.compareAndSet(expect = false, update = true)) {
+                releaseToken()
+                return true
+            }
+            return false
+        }
+    }
+
+    fun acquire(): Token? {
+        synchronized(lock) {
+            if (closed) {
+                return null
+            }
+            count += 1
+            if (count == 1) {
+                timeoutJob?.cancel()
+                timeoutJob = null
+            }
+        }
+        return WakeLockToken()
+    }
+
+    fun release(): Boolean {
+        synchronized(lock) {
+            if (closed) {
+                return false
+            }
+            closed = true
+            timeoutJob?.cancel()
+            timeoutJob = null
+        }
+
+        scope.launch {
+            // Execute the callback
+            callback()
+        }
+
+        return true
+    }
+
+    internal fun releaseToken() {
+        // This function is internal to avoid a synthetic accessor access from [WakeLockToken]
+        synchronized(lock) {
+            count -= 1
+            if (count == 0 && !closed) {
+                timeoutJob = scope.launch {
+                    delay(timeout)
+
+                    synchronized(lock) {
+                        if (closed || count != 0) {
+                            return@launch
+                        }
+                        timeoutJob = null
+                        closed = true
+                    }
+
+                    // Execute the callback
+                    callback()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
index 8083458..a9ec0fa 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
@@ -16,7 +16,6 @@
 
 package androidx.camera.camera2.pipe.wrapper
 
-import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.TotalCaptureResult
@@ -27,17 +26,19 @@
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.UnsafeWrapper
+import androidx.camera.camera2.pipe.impl.Debug
+import androidx.camera.camera2.pipe.impl.Log
 import androidx.camera.camera2.pipe.writeParameter
-import java.io.Closeable
 
 /** Interface around a [CameraDevice] with minor modifications.
  *
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
  */
-interface CameraDeviceWrapper : UnsafeWrapper<CameraDevice>, Closeable {
+interface CameraDeviceWrapper : UnsafeWrapper<CameraDevice> {
     /** @see [CameraDevice.getId] */
     val cameraId: CameraId
 
@@ -102,8 +103,21 @@
     fun createCaptureSession(config: SessionConfigData)
 }
 
+fun CameraDeviceWrapper?.closeWithTrace() {
+    this?.unwrap().closeWithTrace()
+}
+
+fun CameraDevice?.closeWithTrace() {
+    this?.let {
+        Log.info { "$it: Closing" }
+        Debug.trace("$it#close") {
+            it.close()
+        }
+    }
+}
+
 class AndroidCameraDevice(
-    private val cameraCharacteristics: CameraCharacteristics,
+    private val cameraMetadata: CameraMetadata,
     private val cameraDevice: CameraDevice,
     override val cameraId: CameraId
 ) : CameraDeviceWrapper, UnsafeWrapper<CameraDevice> {
@@ -217,18 +231,13 @@
 
         // This compares and sets ONLY the session keys for this camera. Setting parameters that are
         // not listed in availableSessionKeys can cause an unusual amount of extra latency.
-        val sessionKeyNames =
-            cameraCharacteristics.availableSessionKeys.mapTo(HashSet()) { it.name }
+        val sessionKeyNames = cameraMetadata.sessionKeys.map { it.name }
 
         // Iterate template parameters and CHECK BY NAME, as there have been cases where equality
         // checks did not pass.
-        for (parameter in config.sessionParameters) {
-            if (sessionKeyNames.contains(parameter.key.name)) {
-                @Suppress("UNCHECKED_CAST")
-                requestBuilder.writeParameter(
-                    parameter.key as CaptureRequest.Key<Any>,
-                    parameter.value
-                )
+        for ((key, value) in config.sessionParameters) {
+            if (sessionKeyNames.contains(key.name)) {
+                requestBuilder.writeParameter(key, value)
             }
         }
         sessionConfig.sessionParameters = requestBuilder.build()
@@ -248,10 +257,6 @@
         cameraDevice.createReprocessCaptureRequest(inputResult)
     }
 
-    override fun close() = rethrowCamera2Exceptions {
-        cameraDevice.close()
-    }
-
     override fun unwrap(): CameraDevice? {
         return cameraDevice
     }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt
index f47ceebc..373bfa1 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt
@@ -20,6 +20,7 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
 import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.testing.FakeThreads
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -54,7 +55,11 @@
             )
         )
 
-        val cache = CameraMetadataCache(FakeCameras.application)
+        val cache = CameraMetadataCache(
+            FakeCameras.application,
+            FakeThreads.forTests,
+            Permissions(FakeCameras.application)
+        )
 
         val metadata0 = cache.awaitMetadata(camera0)
         val metadata1 = cache.awaitMetadata(camera1)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
index 91d9b4e..27b4852 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
@@ -23,6 +23,7 @@
 import androidx.camera.camera2.pipe.testing.Event
 import androidx.camera.camera2.pipe.testing.FakeRequestListener
 import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import androidx.camera.camera2.pipe.testing.FakeThreads
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
@@ -53,8 +54,8 @@
         // state of results.
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -76,8 +77,8 @@
         // state of results.
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -103,8 +104,8 @@
         // state of results.
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -130,8 +131,8 @@
         // state of results.
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -149,8 +150,8 @@
     fun graphProcessorDoesNotForgetRejectedRequests() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -179,8 +180,8 @@
     fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -222,8 +223,8 @@
     fun graphProcessorSetsRepeatingRequest() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -240,8 +241,8 @@
     fun graphProcessorTracksRepeatingRequest() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -262,8 +263,8 @@
     fun graphProcessorTracksRejectedRepeatingRequests() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -283,8 +284,8 @@
     fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -303,8 +304,8 @@
     fun graphProcessorAbortsQueuedRequests() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
@@ -329,8 +330,8 @@
     fun closingGraphProcessorAbortsSubsequentRequests() {
         runBlocking(Dispatchers.Default) {
             val graphProcessor = GraphProcessorImpl(
+                FakeThreads.forTests,
                 this,
-                Dispatchers.Default,
                 arrayListOf(globalListener)
             )
             graphProcessor.start()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
new file mode 100644
index 0000000..edc640a
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
@@ -0,0 +1,263 @@
+/*
+ * 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.pipe.impl
+
+import android.os.Build
+import android.os.Looper.getMainLooper
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import java.util.concurrent.TimeUnit
+
+@SmallTest
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@OptIn(ExperimentalCoroutinesApi::class)
+class VirtualCameraStateTest {
+    private val mainLooper = shadowOf(getMainLooper())
+    private val cameraId = FakeCameras.create()
+    private val testCamera = FakeCameras.open(cameraId)
+
+    @After
+    fun teardown() {
+        mainLooper.idle()
+        FakeCameras.removeAll()
+    }
+
+    @Test
+    fun virtualCameraStateCanBeDisconnected() = runBlockingTest {
+        // This test asserts that the virtual camera starts in an unopened state and is changed to
+        // "Closed" when disconnect is invoked on the VirtualCamera.
+        val virtualCamera = VirtualCameraState(cameraId)
+        assertThat(virtualCamera.state.value).isInstanceOf(CameraStateUnopened.javaClass)
+
+        virtualCamera.disconnect()
+        assertThat(virtualCamera.state.value).isInstanceOf(CameraStateClosed::class.java)
+
+        val closedState = virtualCamera.state.value as CameraStateClosed
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.APP_DISCONNECTED)
+
+        // Disconnecting a virtual camera does not propagate statistics.
+        assertThat(closedState.cameraErrorCode).isNull()
+        assertThat(closedState.cameraException).isNull()
+        assertThat(closedState.cameraRetryCount).isNull()
+        assertThat(closedState.cameraRetryDurationNs).isNull()
+        assertThat(closedState.cameraOpenDurationNs).isNull()
+        assertThat(closedState.cameraActiveDurationNs).isNull()
+        assertThat(closedState.cameraClosingDurationNs).isNull()
+    }
+
+    @Test
+    fun virtualCameraStateConnectsToFlow() = runBlockingTest {
+        // This test asserts that when a virtual camera is connected to a flow of CameraState
+        // changes that it receives those changes and can be subsequently disconnected, which stops
+        // additional events from being passed to the virtual camera instance.
+        val virtualCamera = VirtualCameraState(cameraId)
+        val cameraState = flowOf(CameraStateOpen(testCamera.cameraDeviceWrapper))
+        virtualCamera.connect(cameraState, object : Token {
+            override fun release(): Boolean {
+                return true
+            }
+        })
+
+        virtualCamera.state.first { it !is CameraStateUnopened }
+
+        assertThat(virtualCamera.state.value).isInstanceOf(CameraStateOpen::class.java)
+        virtualCamera.disconnect()
+        assertThat(virtualCamera.state.value).isInstanceOf(CameraStateClosed::class.java)
+
+        val closedState = virtualCamera.state.value as CameraStateClosed
+        assertThat(closedState.cameraId).isEqualTo(cameraId)
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.APP_DISCONNECTED)
+    }
+
+    @Test
+    fun virtualCameraStateRespondsToClose() = runBlockingTest {
+        // This tests that a listener attached to the virtualCamera.state property will receive all
+        // of the events, starting from CameraStateUnopened.
+        val virtualCamera = VirtualCameraState(cameraId)
+        val states = listOf(
+            CameraStateOpen(testCamera.cameraDeviceWrapper),
+            CameraStateClosing,
+            CameraStateClosed(
+                cameraId,
+                ClosedReason.CAMERA2_ERROR,
+                cameraErrorCode = 5
+            )
+        )
+
+        val events = mutableListOf<CameraState>()
+        val job = launch(start = CoroutineStart.UNDISPATCHED) {
+            virtualCamera.state.collect {
+                events.add(it)
+            }
+        }
+
+        virtualCamera.connect(states.asFlow(), object : Token {
+            override fun release(): Boolean {
+                return true
+            }
+        })
+
+        // Suspend until the state is closed
+        virtualCamera.state.first { it is CameraStateClosed }
+        job.cancelAndJoin()
+
+        val expectedStates = listOf(CameraStateUnopened).plus(states)
+        assertThat(events).containsExactlyElementsIn(expectedStates)
+    }
+}
+
+@SmallTest
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AndroidCameraDeviceTest {
+    private val mainLooper = shadowOf(getMainLooper())
+    private val cameraId = FakeCameras.create()
+    private val testCamera = FakeCameras.open(cameraId)
+    private val now = Metrics.monotonicNanos()
+
+    @After
+    fun teardown() {
+        FakeCameras.removeAll()
+    }
+
+    @Test
+    fun cameraOpensAndGeneratesStats() {
+        mainLooper.idleFor(200, TimeUnit.MILLISECONDS)
+        val listener = AndroidCameraState(
+            testCamera.cameraId,
+            testCamera.metadata,
+            attemptNumber = 1,
+            attemptTimestampNanos = now
+        )
+
+        assertThat(listener.state.value).isInstanceOf(CameraStateUnopened.javaClass)
+
+        // Advance the system clocks.
+        mainLooper.idleFor(200, TimeUnit.MILLISECONDS)
+        listener.onOpened(testCamera.cameraDevice)
+
+        assertThat(listener.state.value).isInstanceOf(CameraStateOpen::class.java)
+        assertThat((listener.state.value as CameraStateOpen).cameraDevice.unwrap())
+            .isSameInstanceAs(testCamera.cameraDevice)
+
+        mainLooper.idleFor(1000, TimeUnit.MILLISECONDS)
+        listener.onClosed(testCamera.cameraDevice)
+        mainLooper.idle()
+
+        assertThat(listener.state.value).isInstanceOf(CameraStateClosed::class.java)
+        val closedState = listener.state.value as CameraStateClosed
+
+        assertThat(closedState.cameraId).isEqualTo(cameraId)
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_CLOSED)
+        assertThat(closedState.cameraRetryCount).isEqualTo(0)
+        assertThat(closedState.cameraException).isNull()
+        assertThat(closedState.cameraRetryDurationNs).isAtLeast(1)
+        assertThat(closedState.cameraOpenDurationNs).isAtLeast(1)
+        assertThat(closedState.cameraActiveDurationNs).isAtLeast(1)
+
+        // Closing duration measures how long "close()" takes to invoke on the camera device.
+        // However, shimming the clocks is difficult.
+        assertThat(closedState.cameraClosingDurationNs).isNotNull()
+    }
+
+    @Test
+    fun multipleCloseEventsReportFirstEvent() {
+        val listener = AndroidCameraState(
+            testCamera.cameraId,
+            testCamera.metadata,
+            attemptNumber = 1,
+            attemptTimestampNanos = now
+        )
+
+        listener.onDisconnected(testCamera.cameraDevice)
+        listener.onError(testCamera.cameraDevice, 42)
+        listener.onClosed(testCamera.cameraDevice)
+
+        mainLooper.idle()
+
+        val closedState = listener.state.value as CameraStateClosed
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_DISCONNECTED)
+    }
+
+    @Test
+    fun closingStateReportsAppClose() {
+        val listener = AndroidCameraState(
+            testCamera.cameraId,
+            testCamera.metadata,
+            attemptNumber = 1,
+            attemptTimestampNanos = now
+        )
+
+        listener.close()
+        mainLooper.idle()
+
+        val closedState = listener.state.value as CameraStateClosed
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.APP_CLOSED)
+    }
+
+    @Test
+    fun closingWithExceptionIsReported() {
+        val listener = AndroidCameraState(
+            testCamera.cameraId,
+            testCamera.metadata,
+            attemptNumber = 1,
+            attemptTimestampNanos = now
+        )
+
+        listener.closeWith(IllegalStateException("Test Exception"))
+        mainLooper.idle()
+
+        val closedState = listener.state.value as CameraStateClosed
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_EXCEPTION)
+    }
+
+    @Test
+    fun errorCodesAreReported() {
+        val listener = AndroidCameraState(
+            testCamera.cameraId,
+            testCamera.metadata,
+            attemptNumber = 1,
+            attemptTimestampNanos = now
+        )
+
+        listener.onError(testCamera.cameraDevice, 24)
+        mainLooper.idle()
+
+        val closedState = listener.state.value as CameraStateClosed
+        assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_ERROR)
+        assertThat(closedState.cameraErrorCode).isEqualTo(24)
+        assertThat(closedState.cameraException).isNull()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/WakeLockTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/WakeLockTest.kt
new file mode 100644
index 0000000..fadd100
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/WakeLockTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.pipe.impl
+
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class WakeLockTest {
+
+    @Test
+    fun testWakeLockInvokesCallbackAfterTokenIsReleased() = runBlocking {
+        val result = CompletableDeferred<Boolean>()
+
+        val wakelock = WakeLock(this) {
+            result.complete(true)
+        }
+
+        wakelock.acquire()!!.release()
+        assertThat(result.await()).isTrue()
+    }
+
+    @Test
+    fun testWakelockDoesNotCompleteUntilAllTokensAreReleased() = runBlocking {
+        val result = CompletableDeferred<Boolean>()
+
+        val wakelock = WakeLock(this) {
+            result.complete(true)
+        }
+
+        val token1 = wakelock.acquire()!!
+        val token2 = wakelock.acquire()!!
+
+        token1.release()
+        delay(50)
+
+        assertThat(result.isActive).isTrue()
+        token2.release()
+
+        assertThat(result.await()).isTrue()
+    }
+
+    @Test
+    fun testClosingWakelockInvokesCallback() = runBlocking {
+        val result = CompletableDeferred<Boolean>()
+        val wakelock = WakeLock(this, 100) {
+            result.complete(true)
+        }
+        wakelock.release()
+        assertThat(result.await()).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
index 5d55047..2f08565 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
@@ -22,20 +22,26 @@
 import android.app.Application
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraManager
+import android.os.Handler
+import android.os.Looper
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.impl.CameraMetadataImpl
+import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
+import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
 import androidx.test.core.app.ApplicationProvider
 import kotlinx.atomicfu.atomic
-import org.robolectric.Shadows
+import org.robolectric.Shadows.shadowOf
 import org.robolectric.shadow.api.Shadow
 import org.robolectric.shadows.ShadowApplication
 import org.robolectric.shadows.ShadowCameraCharacteristics
 import org.robolectric.shadows.ShadowCameraManager
+import java.lang.UnsupportedOperationException
 
 /**
  * Utility class for creating, configuring, and interacting with FakeCamera objects via Robolectric
- *
- * TODO: Implement a utility method to create a fake CameraDevice when robolectric is updated to 4.4
  */
 object FakeCameras {
     private val cameraIds = atomic(0)
@@ -43,7 +49,7 @@
     val application: Application
         get() {
             val app: Application = ApplicationProvider.getApplicationContext()
-            val shadowApp: ShadowApplication = Shadows.shadowOf(app)
+            val shadowApp: ShadowApplication = shadowOf(app)
             shadowApp.grantPermissions(Manifest.permission.CAMERA)
             return app
         }
@@ -51,6 +57,8 @@
     private val cameraManager: CameraManager
         get() = application.getSystemService(Context.CAMERA_SERVICE) as CameraManager
 
+    private val initializedCameraIds = mutableSetOf<CameraId>()
+
     /**
      * This will create, configure, and add the specified CameraCharacteristics to the Robolectric
      * CameraManager, which allows the camera characteristics to be queried for tests and for Fake
@@ -78,11 +86,82 @@
 
         // Add the camera to the camera service
         shadowCameraManager.addCamera(cameraId.value, characteristics)
+        initializedCameraIds.add(cameraId)
 
         return cameraId
     }
 
-    operator fun get(camera: CameraId): CameraCharacteristics {
-        return cameraManager.getCameraCharacteristics(camera.value)
+    operator fun get(fakeCameraId: CameraId): CameraCharacteristics {
+        check(initializedCameraIds.contains(fakeCameraId))
+        return cameraManager.getCameraCharacteristics(fakeCameraId.value)
+    }
+
+    fun open(cameraId: CameraId): FakeCamera {
+        check(initializedCameraIds.contains(cameraId))
+        val characteristics = cameraManager.getCameraCharacteristics(cameraId.value)
+        val metadata = CameraMetadataImpl(cameraId, false, characteristics, emptyMap())
+
+        val callback = CameraStateCallback(cameraId)
+        cameraManager.openCamera(
+            cameraId.value,
+            callback,
+            Handler()
+        )
+        shadowOf(Looper.myLooper()).idle()
+
+        val cameraDevice = callback.camera!!
+        val cameraDeviceWrapper = AndroidCameraDevice(metadata, cameraDevice, cameraId)
+
+        return FakeCamera(
+            cameraId,
+            characteristics,
+            metadata,
+            cameraDevice,
+            cameraDeviceWrapper
+        )
+    }
+
+    /** Remove all fake cameras */
+    fun removeAll() {
+        val shadowCameraManager = Shadow.extract<Any>(
+            cameraManager
+        ) as ShadowCameraManager
+        for (cameraId in initializedCameraIds) {
+            shadowCameraManager.removeCamera(cameraId.value)
+        }
+        initializedCameraIds.clear()
+    }
+
+    /**
+     * The [FakeCamera] instance wraps up several useful objects for use in tests.
+     */
+    data class FakeCamera(
+        val cameraId: CameraId,
+        val characteristics: CameraCharacteristics,
+        val metadata: CameraMetadata,
+        val cameraDevice: CameraDevice,
+        val cameraDeviceWrapper: CameraDeviceWrapper
+    )
+
+    private class CameraStateCallback(private val cameraId: CameraId) :
+        CameraDevice.StateCallback() {
+        var camera: CameraDevice? = null
+        override fun onOpened(cameraDevice: CameraDevice) {
+            check(cameraDevice.id == cameraId.value)
+            this.camera = cameraDevice
+        }
+
+        override fun onDisconnected(camera: CameraDevice) {
+            throw UnsupportedOperationException(
+                "onDisconnected is not expected for Robolectric Camera"
+            )
+        }
+
+        override fun onError(
+            camera: CameraDevice,
+            error: Int
+        ) {
+            throw UnsupportedOperationException("onError is not expected for Robolectric Camera")
+        }
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeThreads.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeThreads.kt
new file mode 100644
index 0000000..2c3c952
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeThreads.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.pipe.testing
+
+import android.os.Handler
+import androidx.camera.camera2.pipe.impl.Threads
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import java.util.concurrent.Executor
+
+object FakeThreads {
+    val forTests = Threads(
+        CoroutineScope(Dispatchers.Default.plus(CoroutineName("CXCP-TestScope"))),
+        Dispatchers.Default.asExecutor(),
+        Dispatchers.Default,
+        Dispatchers.IO.asExecutor(),
+        Dispatchers.IO
+    ) {
+        @Suppress("DEPRECATION")
+        (Handler())
+    }
+
+    fun fromExecutor(executor: Executor): Threads {
+        return fromDispatcher(executor.asCoroutineDispatcher())
+    }
+
+    fun fromDispatcher(dispatcher: CoroutineDispatcher): Threads {
+        val executor = dispatcher.asExecutor()
+
+        return Threads(
+            CoroutineScope(dispatcher.plus(CoroutineName("CXCP-TestScope"))),
+            defaultExecutor = executor,
+            defaultDispatcher = dispatcher,
+            ioExecutor = executor,
+            ioDispatcher = dispatcher
+        ) {
+            @Suppress("DEPRECATION")
+            (Handler())
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index 4917fa9..d0441f7 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -30,6 +30,7 @@
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Build;
 import android.util.Size;
 import android.view.WindowManager;
@@ -90,9 +91,7 @@
 @SmallTest
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP,
-        maxSdk = Build.VERSION_CODES.P //TODO (b/149669465) : Some robolectric tests will fail on Q
-)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public final class Camera2DeviceSurfaceManagerTest {
     private static final String LEGACY_CAMERA_ID = "0";
     private static final String LIMITED_CAMERA_ID = "1";
@@ -548,10 +547,21 @@
         ((ShadowCameraManager) Shadow.extract(cameraManager))
                 .addCamera(cameraId, characteristics);
 
-        shadowCharacteristics.set(
-                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
-                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
-                        mSupportedFormats, mSupportedSizes));
+        // Current robolectric can support to directly mock a StreamConfigurationMap object if
+        // the testing platform target is equal to or newer than API level 23. For API level 21
+        // or 22 testing platform target, keep the original method to create a
+        // StreamConfigurationMap object via reflection.
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(mSupportedFormats,
+                            mSupportedSizes));
+        } else {
+            StreamConfigurationMap mockMap = mock(StreamConfigurationMap.class);
+            when(mockMap.getOutputSizes(anyInt())).thenReturn(mSupportedSizes);
+            shadowCharacteristics.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    mockMap);
+        }
 
         @CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
                 lensFacing);
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
index b12db61..e444a45b 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
@@ -26,6 +26,7 @@
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Build;
 import android.util.Size;
 
@@ -72,9 +73,7 @@
 @SmallTest
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP,
-        maxSdk = Build.VERSION_CODES.P //TODO (b/149669465) : Some robolectric tests will fail on Q
-)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class SupportedSizeConstraintsTest {
     private static final String BACK_CAMERA_ID = "0";
     private static final int DEFAULT_SENSOR_ORIENTATION = 90;
@@ -223,10 +222,21 @@
 
         int[] supportedFormats = mSupportedFormats;
 
-        shadowCharacteristics.set(
-                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
-                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(supportedFormats,
-                        supportedSizes));
+        // Current robolectric can support to directly mock a StreamConfigurationMap object if
+        // the testing platform target is equal to or newer than API level 23. For API level 21
+        // or 22 testing platform target, keep the original method to create a
+        // StreamConfigurationMap object via reflection.
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(supportedFormats,
+                            supportedSizes));
+        } else {
+            StreamConfigurationMap mockMap = mock(StreamConfigurationMap.class);
+            when(mockMap.getOutputSizes(anyInt())).thenReturn(supportedSizes);
+            shadowCharacteristics.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    mockMap);
+        }
 
         @CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
                 CameraCharacteristics.LENS_FACING_BACK);
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index 2064fb8..08fb3e5 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -29,6 +29,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
 import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Build;
 import android.util.Pair;
 import android.util.Rational;
@@ -93,9 +94,7 @@
 @SmallTest
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP,
-        maxSdk = Build.VERSION_CODES.P //TODO (b/149669465) : Some robolectric tests will fail on Q
-)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public final class SupportedSurfaceCombinationTest {
     private static final String CAMERA_ID = "0";
     private static final int DEFAULT_SENSOR_ORIENTATION = 90;
@@ -1899,10 +1898,21 @@
         int[] supportedFormats = isRawSupported(capabilities)
                 ? mSupportedFormatsWithRaw : mSupportedFormats;
 
-        shadowCharacteristics.set(
-                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
-                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(supportedFormats,
-                        supportedSizes));
+        // Current robolectric can support to directly mock a StreamConfigurationMap object if
+        // the testing platform target is equal to or newer than API level 23. For API level 21
+        // or 22 testing platform target, keep the original method to create a
+        // StreamConfigurationMap object via reflection.
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(supportedFormats,
+                            supportedSizes));
+        } else {
+            StreamConfigurationMap mockMap = mock(StreamConfigurationMap.class);
+            when(mockMap.getOutputSizes(anyInt())).thenReturn(supportedSizes);
+            shadowCharacteristics.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    mockMap);
+        }
 
         @CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
                 CameraCharacteristics.LENS_FACING_BACK);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.java
index 187e794..5fb4ba1 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.java
@@ -19,10 +19,15 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.util.Rational;
+import android.view.Surface;
+
 import androidx.camera.core.UseCase;
+import androidx.camera.core.ViewPort;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.testing.fakes.FakeCamera;
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
@@ -174,4 +179,35 @@
 
         verify(callback).onUnbind();
     }
+
+    @Test
+    public void addExistingUseCase_viewPortUpdated()
+            throws CameraUseCaseAdapter.CameraException {
+        Rational aspectRatio1 = new Rational(1, 1);
+        Rational aspectRatio2 = new Rational(2, 1);
+
+        // Arrange: set up adapter with aspect ratio 1.
+        CameraUseCaseAdapter cameraUseCaseAdapter = new CameraUseCaseAdapter(mFakeCamera,
+                mFakeCameraSet,
+                mFakeCameraDeviceSurfaceManager);
+        cameraUseCaseAdapter.setViewPort(
+                new ViewPort.Builder(aspectRatio1, Surface.ROTATION_0).build());
+        FakeUseCase fakeUseCase = spy(new FakeUseCase());
+        cameraUseCaseAdapter.addUseCases(Collections.singleton(fakeUseCase));
+        // Use case gets aspect ratio 1
+        assertThat(fakeUseCase.getViewPortCropRect()).isNotNull();
+        assertThat(new Rational(fakeUseCase.getViewPortCropRect().width(),
+                fakeUseCase.getViewPortCropRect().height())).isEqualTo(aspectRatio1);
+
+        // Act: set aspect ratio 2 and attach the same use case.
+        reset(fakeUseCase);
+        cameraUseCaseAdapter.setViewPort(
+                new ViewPort.Builder(aspectRatio2, Surface.ROTATION_0).build());
+        cameraUseCaseAdapter.addUseCases(Collections.singleton(fakeUseCase));
+
+        // Assert: the viewport has aspect ratio 2.
+        assertThat(fakeUseCase.getViewPortCropRect()).isNotNull();
+        assertThat(new Rational(fakeUseCase.getViewPortCropRect().width(),
+                fakeUseCase.getViewPortCropRect().height())).isEqualTo(aspectRatio2);
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index a2ca7d4..87be783 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -564,8 +564,7 @@
      */
     @RestrictTo(Scope.LIBRARY)
     @Nullable
-    @SuppressWarnings("KotlinPropertyAccess")
-    protected Rect getViewPortCropRect() {
+    public Rect getViewPortCropRect() {
         return mViewPortCropRect;
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 6814c12..365a3a4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -157,7 +157,7 @@
 
             for (UseCase useCase : useCases) {
                 if (mUseCases.contains(useCase)) {
-                    Log.e(TAG, "Attempting to attach already attached UseCase");
+                    Log.d(TAG, "Attempting to attach already attached UseCase");
                 } else {
                     useCaseListAfterUpdate.add(useCase);
                     newUseCases.add(useCase);
@@ -189,7 +189,8 @@
                         mViewPort.getLayoutDirection(),
                         suggestedResolutionsMap);
                 for (UseCase useCase : useCases) {
-                    useCase.setViewPortCropRect(cropRectMap.get(useCase));
+                    useCase.setViewPortCropRect(
+                            Preconditions.checkNotNull(cropRectMap.get(useCase)));
                 }
             }
 
@@ -279,38 +280,42 @@
             @NonNull List<UseCase> currentUseCases) {
         List<SurfaceConfig> existingSurfaces = new ArrayList<>();
         String cameraId = mCameraInternal.getCameraInfoInternal().getCameraId();
+        Map<UseCase, Size> suggestedResolutions = new HashMap<>();
 
-        Map<UseCaseConfig<?>, UseCase> configToUseCaseMap = new HashMap<>();
-
+        // Get resolution for current use cases.
         for (UseCase useCase : currentUseCases) {
             SurfaceConfig surfaceConfig =
                     mCameraDeviceSurfaceManager.transformSurfaceConfig(cameraId,
                             useCase.getImageFormat(),
                             useCase.getAttachedSurfaceResolution());
             existingSurfaces.add(surfaceConfig);
+            suggestedResolutions.put(useCase, useCase.getAttachedSurfaceResolution());
         }
 
-        for (UseCase useCase : newUseCases) {
-            UseCaseConfig.Builder<?, ?, ?> defaultBuilder = useCase.getDefaultBuilder(
-                    mCameraInternal.getCameraInfoInternal());
+        // Calculate resolution for new use cases.
+        if (!newUseCases.isEmpty()) {
+            Map<UseCaseConfig<?>, UseCase> configToUseCaseMap = new HashMap<>();
+            for (UseCase useCase : newUseCases) {
+                UseCaseConfig.Builder<?, ?, ?> defaultBuilder = useCase.getDefaultBuilder(
+                        mCameraInternal.getCameraInfoInternal());
 
-            // Combine with default configuration.
-            UseCaseConfig<?> combinedUseCaseConfig =
-                    useCase.applyDefaults(useCase.getUseCaseConfig(),
-                            defaultBuilder);
-            configToUseCaseMap.put(combinedUseCaseConfig, useCase);
+                // Combine with default configuration.
+                UseCaseConfig<?> combinedUseCaseConfig =
+                        useCase.applyDefaults(useCase.getUseCaseConfig(),
+                                defaultBuilder);
+                configToUseCaseMap.put(combinedUseCaseConfig, useCase);
+            }
+
+            // Get suggested resolutions and update the use case session configuration
+            Map<UseCaseConfig<?>, Size> useCaseConfigSizeMap = mCameraDeviceSurfaceManager
+                    .getSuggestedResolutions(cameraId, existingSurfaces,
+                            new ArrayList<>(configToUseCaseMap.keySet()));
+
+            for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
+                suggestedResolutions.put(entry.getValue(),
+                        useCaseConfigSizeMap.get(entry.getKey()));
+            }
         }
-
-        // Get suggested resolutions and update the use case session configuration
-        Map<UseCaseConfig<?>, Size> useCaseConfigSizeMap = mCameraDeviceSurfaceManager
-                .getSuggestedResolutions(cameraId, existingSurfaces,
-                        new ArrayList<>(configToUseCaseMap.keySet()));
-
-        Map<UseCase, Size> suggestedResolutions = new HashMap<>();
-        for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
-            suggestedResolutions.put(entry.getValue(), useCaseConfigSizeMap.get(entry.getKey()));
-        }
-
         return suggestedResolutions;
     }
 
@@ -333,6 +338,7 @@
      */
     public static final class CameraId {
         private final List<String> mIds;
+
         CameraId(LinkedHashSet<CameraInternal> cameraInternals) {
             mIds = new ArrayList<>();
             for (CameraInternal cameraInternal : cameraInternals) {
diff --git a/compose/compose-runtime/api/restricted_current.txt b/compose/compose-runtime/api/restricted_current.txt
index 4bf593f..bb35e2b 100644
--- a/compose/compose-runtime/api/restricted_current.txt
+++ b/compose/compose-runtime/api/restricted_current.txt
@@ -946,6 +946,7 @@
     method public static inline <T extends androidx.compose.runtime.snapshots.StateRecord, R> R! writable(T, androidx.compose.runtime.snapshots.StateObject state, kotlin.jvm.functions.Function1<? super T,? extends R> block);
     method @kotlin.PublishedApi internal static <T extends androidx.compose.runtime.snapshots.StateRecord> T writableRecord(T, androidx.compose.runtime.snapshots.StateObject state, androidx.compose.runtime.snapshots.Snapshot snapshot);
     field @kotlin.PublishedApi internal static final Object lock;
+    field @kotlin.PublishedApi internal static final androidx.compose.runtime.snapshots.Snapshot snapshotInitializer;
   }
 
   @androidx.compose.runtime.Stable public final class SnapshotStateList<T> implements kotlin.jvm.internal.markers.KMutableList java.util.List<T> androidx.compose.runtime.snapshots.StateObject {
diff --git a/compose/compose-runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt b/compose/compose-runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt
new file mode 100644
index 0000000..7a3b9ac
--- /dev/null
+++ b/compose/compose-runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/AndroidSnapshotTests.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.compose.runtime
+
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AndroidSnapshotTests : BaseComposeTest() {
+    @get:Rule
+    override val activityRule = makeTestActivityRule()
+
+    @OptIn(ExperimentalComposeApi::class)
+    @Test // regression test for b/163903673
+    fun testCommittingInABackgroundThread() {
+        val states = Array(10000) { mutableStateOf(0) }
+        var stop = false
+        object : Thread() {
+            override fun run() {
+                while (!stop) {
+                    for (state in states) {
+                        state.value = state.value + 1
+                    }
+                    sleep(1)
+                }
+            }
+        }.start()
+        try {
+            val unregister = Snapshot.registerApplyObserver { changed, _ ->
+                // Try to catch a concurrent modification exception
+                val iterator = changed.iterator()
+                while (iterator.hasNext()) {
+                    iterator.next()
+                }
+            }
+            try {
+                repeat(1000) {
+                    activityRule.activity.uiThread {
+                        Snapshot.sendApplyNotifications()
+                    }
+                }
+            } finally {
+                unregister()
+            }
+        } finally {
+            stop = true
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 0785ddc..0655c7a 100644
--- a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -1371,6 +1371,15 @@
     openSnapshots = openSnapshots.set(it.id)
 }
 
+// A value to use to initialize the snapshot local variable of writable below. The value of this
+// doesn't matter as it is just used to initialize the local that is immediately overwritten by
+// Snapshot.current. This is done to avoid a compiler error complaining that the var has not been
+// initialized. This can be removed once contracts are out of experimental; then we can mark sync
+// with the correct contracts so the compiler would be able to figure out that the variable is
+// initialized.
+@PublishedApi
+internal val snapshotInitializer: Snapshot = currentGlobalSnapshot
+
 private fun <T> takeNewGlobalSnapshot(
     previousGlobalSnapshot: Snapshot,
     block: (invalid: SnapshotIdSet) -> T
@@ -1602,8 +1611,15 @@
  * called for the first state record in a state object. A record is writable if it was created in
  * the current mutable snapshot.
  */
-inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R =
-    this.writable(state, Snapshot.current, block)
+inline fun <T : StateRecord, R> T.writable(state: StateObject, block: T.() -> R): R {
+    var snapshot: Snapshot = snapshotInitializer
+    return sync {
+        snapshot = Snapshot.current
+        this.writableRecord(state, snapshot).block()
+    }.also {
+        notifyWrite(snapshot, state)
+    }
+}
 
 /**
  * Produce a set of optimistic merges of the state records, this is performed outside the
diff --git a/datastore/datastore-core/api/current.txt b/datastore/datastore-core/api/current.txt
index c29edba..80f8b50 100644
--- a/datastore/datastore-core/api/current.txt
+++ b/datastore/datastore-core/api/current.txt
@@ -23,8 +23,8 @@
 
   public final class DataStoreFactory {
     ctor public DataStoreFactory();
-    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<T>>> migrationProducers = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
-    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<T>>> migrationProducers = listOf());
+    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<T>> migrations = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
+    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<T>> migrations = listOf());
     method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null);
     method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer);
   }
@@ -47,13 +47,17 @@
 
 package androidx.datastore.migrations {
 
-  public interface MigrationFromSharedPreferences<T> {
-    method public suspend Object? migrate(androidx.datastore.migrations.SharedPreferencesView prefs, T? currentData, kotlin.coroutines.Continuation<? super T> p);
-    method public default suspend Object? shouldMigrate(T? currentData, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+  public final class SharedPreferencesMigration<T> implements androidx.datastore.DataMigration<T> {
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p) throws java.io.IOException;
+    method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
+    method public suspend Object? shouldMigrate(T? currentData, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
   }
 
-  public final class SharedPreferencesMigration {
-    method public static <T> kotlin.jvm.functions.Function0<androidx.datastore.DataMigration<T>> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, androidx.datastore.migrations.MigrationFromSharedPreferences<T> migration, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+  public final class SharedPreferencesMigrationKt {
   }
 
   public final class SharedPreferencesView {
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index c29edba..80f8b50 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -23,8 +23,8 @@
 
   public final class DataStoreFactory {
     ctor public DataStoreFactory();
-    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<T>>> migrationProducers = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
-    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<T>>> migrationProducers = listOf());
+    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<T>> migrations = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
+    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<T>> migrations = listOf());
     method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null);
     method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer);
   }
@@ -47,13 +47,17 @@
 
 package androidx.datastore.migrations {
 
-  public interface MigrationFromSharedPreferences<T> {
-    method public suspend Object? migrate(androidx.datastore.migrations.SharedPreferencesView prefs, T? currentData, kotlin.coroutines.Continuation<? super T> p);
-    method public default suspend Object? shouldMigrate(T? currentData, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+  public final class SharedPreferencesMigration<T> implements androidx.datastore.DataMigration<T> {
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p) throws java.io.IOException;
+    method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
+    method public suspend Object? shouldMigrate(T? currentData, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
   }
 
-  public final class SharedPreferencesMigration {
-    method public static <T> kotlin.jvm.functions.Function0<androidx.datastore.DataMigration<T>> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, androidx.datastore.migrations.MigrationFromSharedPreferences<T> migration, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+  public final class SharedPreferencesMigrationKt {
   }
 
   public final class SharedPreferencesView {
diff --git a/datastore/datastore-core/api/restricted_current.txt b/datastore/datastore-core/api/restricted_current.txt
index c29edba..80f8b50 100644
--- a/datastore/datastore-core/api/restricted_current.txt
+++ b/datastore/datastore-core/api/restricted_current.txt
@@ -23,8 +23,8 @@
 
   public final class DataStoreFactory {
     ctor public DataStoreFactory();
-    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<T>>> migrationProducers = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
-    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<T>>> migrationProducers = listOf());
+    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<T>> migrations = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
+    method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<T>> migrations = listOf());
     method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer, androidx.datastore.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler = null);
     method public <T> androidx.datastore.DataStore<T> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.Serializer<T> serializer);
   }
@@ -47,13 +47,17 @@
 
 package androidx.datastore.migrations {
 
-  public interface MigrationFromSharedPreferences<T> {
-    method public suspend Object? migrate(androidx.datastore.migrations.SharedPreferencesView prefs, T? currentData, kotlin.coroutines.Continuation<? super T> p);
-    method public default suspend Object? shouldMigrate(T? currentData, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+  public final class SharedPreferencesMigration<T> implements androidx.datastore.DataMigration<T> {
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> shouldRunMigration, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, boolean deleteEmptyPreferences, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    ctor public SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, kotlin.jvm.functions.Function3<? super androidx.datastore.migrations.SharedPreferencesView,? super T,? super kotlin.coroutines.Continuation<? super T>,?> migrate);
+    method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p) throws java.io.IOException;
+    method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
+    method public suspend Object? shouldMigrate(T? currentData, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
   }
 
-  public final class SharedPreferencesMigration {
-    method public static <T> kotlin.jvm.functions.Function0<androidx.datastore.DataMigration<T>> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, androidx.datastore.migrations.MigrationFromSharedPreferences<T> migration, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+  public final class SharedPreferencesMigrationKt {
   }
 
   public final class SharedPreferencesView {
diff --git a/datastore/datastore-core/src/androidTest/java/migrations/SharedPreferencesMigrationTest.kt b/datastore/datastore-core/src/androidTest/java/migrations/SharedPreferencesMigrationTest.kt
index b0edfb0..bb90413 100644
--- a/datastore/datastore-core/src/androidTest/java/migrations/SharedPreferencesMigrationTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/migrations/SharedPreferencesMigrationTest.kt
@@ -58,20 +58,12 @@
 
     @Test
     fun testShouldMigrateSkipsMigration() = runBlockingTest {
-        val migration = object : MigrationFromSharedPreferences<Byte> {
-            override suspend fun shouldMigrate(currentData: Byte) = false
-
-            override suspend fun migrate(
-                prefs: SharedPreferencesView,
-                currentData: Byte
-            ) = throw IllegalStateException("Migration is skipped.")
-        }
-
-        val sharedPrefsMigration = SharedPreferencesMigration(
+        val sharedPrefsMigration = SharedPreferencesMigration<Byte>(
             context = context,
             sharedPreferencesName = sharedPrefsName,
-            migration = migration
-        )
+            shouldRunMigration = { false }) { _: SharedPreferencesView, _: Byte ->
+            throw IllegalStateException("Migration should've been skipped.")
+        }
 
         val dataStore = getDataStoreWithMigrations(listOf(sharedPrefsMigration))
 
@@ -91,28 +83,17 @@
                 .putInt(notMigratedKey, 123).commit()
         ).isTrue()
 
-        val migration = object : MigrationFromSharedPreferences<Byte> {
-            override suspend fun shouldMigrate(currentData: Byte) = true
-
-            override suspend fun migrate(
-                prefs: SharedPreferencesView,
-                currentData: Byte
-            ): Byte {
-                assertThat(prefs.getInt(includedKey, -1)).isEqualTo(includedVal)
-                assertThrows<IllegalStateException> { prefs.getInt(notMigratedKey, -1) }
-
-                assertThat(prefs.getAll()).isEqualTo(mapOf(includedKey to includedVal))
-
-                return 99.toByte()
-            }
-        }
-
         val sharedPrefsMigration = SharedPreferencesMigration(
             context = context,
             sharedPreferencesName = sharedPrefsName,
-            migration = migration,
             keysToMigrate = setOf(includedKey)
-        )
+        ) { prefs: SharedPreferencesView, _: Byte ->
+            assertThat(prefs.getInt(includedKey, -1)).isEqualTo(includedVal)
+            assertThrows<IllegalStateException> { prefs.getInt(notMigratedKey, -1) }
+            assertThat(prefs.getAll()).isEqualTo(mapOf(includedKey to includedVal))
+
+            99.toByte()
+        }
 
         val dataStore = getDataStoreWithMigrations(listOf(sharedPrefsMigration))
 
@@ -135,27 +116,17 @@
                 .commit()
         ).isTrue()
 
-        val migration = object : MigrationFromSharedPreferences<Byte> {
-            override suspend fun shouldMigrate(currentData: Byte) = true
-
-            override suspend fun migrate(
-                prefs: SharedPreferencesView,
-                currentData: Byte
-            ): Byte {
-                assertThat(prefs.getInt(key1, -1)).isEqualTo(val1)
-                assertThat(prefs.getInt(key2, -1)).isEqualTo(val2)
-
-                assertThat(prefs.getAll()).isEqualTo(mapOf(key1 to val1, key2 to val2))
-
-                return 99.toByte()
-            }
-        }
-
         val sharedPrefsMigration = SharedPreferencesMigration(
             context = context,
-            sharedPreferencesName = sharedPrefsName,
-            migration = migration
-        )
+            sharedPreferencesName = sharedPrefsName
+        ) { prefs: SharedPreferencesView, _: Byte ->
+            assertThat(prefs.getInt(key1, -1)).isEqualTo(val1)
+            assertThat(prefs.getInt(key2, -1)).isEqualTo(val2)
+
+            assertThat(prefs.getAll()).isEqualTo(mapOf(key1 to val1, key2 to val2))
+
+            99.toByte()
+        }
 
         val dataStore = getDataStoreWithMigrations(listOf(sharedPrefsMigration))
 
@@ -165,12 +136,12 @@
     }
 
     private fun getDataStoreWithMigrations(
-        migrationProducers: List<() -> DataMigration<Byte>>
+        migrations: List<DataMigration<Byte>>
     ): DataStore<Byte> {
         return DataStoreFactory().create(
             produceFile = { datastoreFile },
             serializer = TestingSerializer(),
-            migrationProducers = migrationProducers,
+            migrations = migrations,
             scope = TestCoroutineScope()
         )
     }
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/DataMigration.kt b/datastore/datastore-core/src/main/java/androidx/datastore/DataMigration.kt
index 892f4cc..aca7f29 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/DataMigration.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/DataMigration.kt
@@ -17,15 +17,23 @@
 package androidx.datastore
 
 /**
- * Interface for migrations to DataStore. If you're migrating from SharedPreferences see
- * [SharedPreferencesMigration].
+ * Interface for migrations to DataStore. Methods on this migration ([shouldMigrate], [migrate]
+ * and [cleanUp]) may be called multiple times, so their implementations must be idempotent.
+ * These methods may be called multiple times if DataStore encounters issues when writing the
+ * newly migrated data to disk or if any migration installed in the same DataStore throws an
+ * Exception.
+ *
+ * If you're migrating from SharedPreferences see [SharedPreferencesMigration].
  */
 interface DataMigration<T> {
 
     /**
      * Return whether this migration needs to be performed. If this returns false, no migration or
      * cleanup will occur. Apps should do the cheapest possible check to determine if this migration
-     * should run, since this will be called every time the DataStore is initialized.
+     * should run, since this will be called every time the DataStore is initialized. This method
+     * may be run multiple times when any failure is encountered.
+     *
+     * Note that this will always be called before each call to [migrate].
      *
      * @param currentData the current data (which might already populated from previous runs of this
      * or other migrations)
@@ -37,7 +45,9 @@
      * multiple times. If migrate fails, DataStore will not commit any data to disk, cleanUp will
      * not be called, and the exception will be propagated back to the DataStore call that
      * triggered the migration. Future calls to DataStore will result in DataMigrations being
-     * attempted again.
+     * attempted again. This method may be run multiple times when any failure is encountered.
+     *
+     * Note that this will always be called before a call to [cleanUp].
      *
      * @param currentData the current data (it might be populated from other migrations or from
      * manual changes before this migration was added to the app)
@@ -49,7 +59,8 @@
      * Clean up any old state/data that was migrated into the DataStore. This will not be called
      * if the migration fails. If cleanUp throws an exception, the exception will be propagated
      * back to the DataStore call that triggered the migration and future calls to DataStore will
-     * result in DataMigrations being attempted again.
+     * result in DataMigrations being attempted again. This method may be run multiple times when
+     * any failure is encountered.
      */
     suspend fun cleanUp()
 }
\ No newline at end of file
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/DataMigrationInitializer.kt b/datastore/datastore-core/src/main/java/androidx/datastore/DataMigrationInitializer.kt
index 42ea187..e5dea0f 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/DataMigrationInitializer.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/DataMigrationInitializer.kt
@@ -24,18 +24,13 @@
         /**
          * Creates an initializer from DataMigrations for use with DataStore.
          *
-         * @param migrationTaskFactories A list of functions that return migrations that will be
-         * included in the initializer. If the DataMigration contains any state, the function
-         * should return a new migration each time it is called.
+         * @param migrations A list of migrations that will be included in the initializer.
          * @return The initializer which includes the data migrations returned from the factory
          * functions.
          */
-        fun <T> getInitializer(migrationTaskFactories: List<() -> DataMigration<T>>):
-                suspend (api: InitializerApi<T>) -> Unit {
-            return { api ->
-                val migrations = migrationTaskFactories.map { it() }
-                runMigrations(migrations, api)
-            }
+        fun <T> getInitializer(migrations: List<DataMigration<T>>):
+                suspend (api: InitializerApi<T>) -> Unit = { api ->
+            runMigrations(migrations, api)
         }
 
         private suspend fun <T> runMigrations(
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/DataStoreFactory.kt b/datastore/datastore-core/src/main/java/androidx/datastore/DataStoreFactory.kt
index 5e7745f..969495e 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/DataStoreFactory.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/DataStoreFactory.kt
@@ -44,23 +44,23 @@
      * @param corruptionHandler The corruptionHandler is invoked if DataStore encounters a
      * [CorruptionException] when attempting to read data. CorruptionExceptions are thrown by
      * serializers when data can not be de-serialized.
-     * @param migrationProducers Migrations are run before any access to data can occur. Migrations
-     * must be idempotent.
+     * @param migrations Migrations are run before any access to data can occur. Migrations must
+     * be idempotent.
      * @param scope The scope in which IO operations and transform functions will execute.
      */
-    @JvmOverloads
+    @JvmOverloads // Generate constructors for default params for java users.
     fun <T> create(
         produceFile: () -> File,
         serializer: Serializer<T>,
         corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
-        migrationProducers: List<() -> DataMigration<T>> = listOf(),
+        migrations: List<DataMigration<T>> = listOf(),
         scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
     ): DataStore<T> =
         SingleProcessDataStore(
             produceFile = produceFile,
             serializer = serializer,
             corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
-            initTasksList = listOf(DataMigrationInitializer.getInitializer(migrationProducers)),
+            initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
             scope = scope
         )
 }
\ No newline at end of file
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt b/datastore/datastore-core/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
index 4a26721..63826ca 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:JvmName("SharedPreferencesMigration")
-
 package androidx.datastore.migrations
 
 import android.content.Context
@@ -25,18 +23,19 @@
 import java.io.IOException
 
 /**
- * A DataMigration which migrates SharedPreferences to DataStore.
+ * DataMigration from SharedPreferences to DataStore.
  *
- * Note: this accesses the SharedPreferences using MODE_PRIVATE.
- */
-internal val MIGRATE_ALL_KEYS = null
-
-/**
- * Returns a factory function which creates a SharedPreferences Data Migration.
+ * Example usage:
+ *
+ * val sharedPrefsMigration = SharedPreferencesMigration(
+ *   context,
+ *   mySharedPreferencesName
+ * ) { prefs: SharedPreferencesView, myData: MyData ->
+ *    myData.toBuilder().setCounter(prefs.getCounter(COUNTER_KEY, default = 0)).build()
+ * }
  *
  * @param context Context used for getting SharedPreferences.
  * @param sharedPreferencesName The name of the SharedPreferences.
- * @param migration The mapping function for the migration.
  * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore
  * .Preferences with their same values. If the key is already present in the new Preferences, the key
  * will not be migrated again. If the key is not present in the SharedPreferences it will not be
@@ -48,49 +47,93 @@
  * SharedPreferences to begin with then the (potentially) empty SharedPreferences won't be
  * cleaned up by this option. This functionality is best effort - if there is an issue deleting
  * the SharedPreferences file it will be silently ignored.
+ * @param migrate maps SharedPreferences into T. Implementations should be idempotent
+ * since this may be called multiple times. See [DataMigration.migrate] for more
+ * information. The lambda accepts a SharedPreferencesView which is the view of the
+ * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+ * the current data. The function must return the migrated data.
  */
-fun <T> SharedPreferencesMigration(
-    context: Context,
-    sharedPreferencesName: String,
-    migration: MigrationFromSharedPreferences<T>,
+class SharedPreferencesMigration<T>
+@JvmOverloads // Generate constructors for default params for java users.
+constructor(
+    private val context: Context,
+    private val sharedPreferencesName: String,
     keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
-    deleteEmptyPreferences: Boolean = true
-): () -> DataMigration<T> = {
-    SharedPreferencesDataMigration(
-        context,
-        sharedPreferencesName,
-        migration,
-        keysToMigrate?.toMutableSet(),
-        deleteEmptyPreferences
-    )
-}
+    private val deleteEmptyPreferences: Boolean = true,
+    private val shouldRunMigration: suspend (T) -> Boolean = { true },
+    private val migrate: suspend (SharedPreferencesView, T) -> T
+) : DataMigration<T> {
 
-/**
- * User implemented migration interface. Contains logic for mapping SharedPreferences data to T.
- */
-interface MigrationFromSharedPreferences<T> {
-    /**
-     * Optional method that should return false if the migration should be skipped. This can
-     * be useful to stop unnecessary calls into SharedPreferences. This can be implemented by
-     * including a field in your data that specifies whether your migration has already been
-     * run.
-     *
-     * @param currentData the current data (it might already populated from this or other
-     * migrations)
-     * @return Whether or not this migration should run.
-     */
-    suspend fun shouldMigrate(currentData: T): Boolean = true
+    private val sharedPrefs: SharedPreferences by lazy {
+        context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE)
+    }
 
-    /**
-     * Perform the migration. Implementations should be idempotent since this may be called
-     * multiple times. See {@code DataMigration#migrate} for more information.
-     *
-     * @param prefs the view of the SharedPreferences to migrate from.
-     * @param currentData the current data (it might be populated from other migrations or from
-     * manual changes before this migration was added to the app)
-     * @return The migrated data.
-     */
-    suspend fun migrate(prefs: SharedPreferencesView, currentData: T): T
+    private val keySet: MutableSet<String> by lazy {
+        (keysToMigrate ?: sharedPrefs.all.keys).toMutableSet()
+    }
+
+    override suspend fun shouldMigrate(currentData: T): Boolean {
+        if (!shouldRunMigration(currentData)) {
+            return false
+        }
+
+        return keySet.any(sharedPrefs::contains)
+    }
+
+    override suspend fun migrate(currentData: T): T =
+        migrate(
+            SharedPreferencesView(
+                sharedPrefs,
+                keySet
+            ), currentData
+        )
+
+    @Throws(IOException::class)
+    override suspend fun cleanUp() {
+        val sharedPrefsEditor = sharedPrefs.edit()
+
+        for (key in keySet) {
+            sharedPrefsEditor.remove(key)
+        }
+
+        if (!sharedPrefsEditor.commit()) {
+            throw IOException(
+                "Unable to delete migrated keys from SharedPreferences: $sharedPreferencesName"
+            )
+        }
+
+        if (deleteEmptyPreferences && sharedPrefs.all.isEmpty()) {
+            deleteSharedPreferences(context, sharedPreferencesName)
+        }
+
+        keySet.clear()
+    }
+
+    private fun deleteSharedPreferences(context: Context, name: String) {
+        if (android.os.Build.VERSION.SDK_INT >= 24) {
+            if (!context.deleteSharedPreferences(name)) {
+                throw IOException("Unable to delete SharedPreferences: $name")
+            }
+            return
+        }
+
+        // Context.deleteSharedPreferences is SDK 24+, so we have to reproduce the definition
+        val prefsFile = getSharedPrefsFile(context, name)
+        val prefsBackup = getSharedPrefsBackup(prefsFile)
+
+        // Silently continue if we aren't able to delete the Shared Preferences File.
+        prefsFile.delete()
+        prefsBackup.delete()
+    }
+
+    // ContextImpl.getSharedPreferencesPath is private, so we have to reproduce the definition
+    private fun getSharedPrefsFile(context: Context, name: String): File {
+        val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
+        return File(prefsDir, "$name.xml")
+    }
+
+    // SharedPreferencesImpl.makeBackupFile is private, so we have to reproduce the definition
+    private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak")
 }
 
 /**
@@ -183,81 +226,4 @@
     }
 }
 
-private class SharedPreferencesDataMigration<T> internal constructor(
-    private val context: Context,
-    private val sharedPreferencesName: String,
-    private val migration: MigrationFromSharedPreferences<T>,
-    keysToMigrate: MutableSet<String>?,
-    private val deleteEmptyPreferences: Boolean
-) : DataMigration<T> {
-    private val sharedPrefs: SharedPreferences by lazy {
-        context.getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE)
-    }
-
-    private val keySet: MutableSet<String> by lazy {
-        keysToMigrate ?: sharedPrefs.all.keys.toMutableSet()
-    }
-
-    override suspend fun shouldMigrate(currentData: T): Boolean {
-        if (!migration.shouldMigrate(currentData)) {
-            return false
-        }
-
-        return keySet.any(sharedPrefs::contains)
-    }
-
-    override suspend fun migrate(currentData: T): T =
-        migration.migrate(
-            SharedPreferencesView(
-                sharedPrefs,
-                keySet
-            ), currentData
-        )
-
-    @Throws(IOException::class)
-    override suspend fun cleanUp() {
-        val sharedPrefsEditor = sharedPrefs.edit()
-
-        for (key in keySet) {
-            sharedPrefsEditor.remove(key)
-        }
-
-        if (!sharedPrefsEditor.commit()) {
-            throw IOException(
-                "Unable to delete migrated keys from SharedPreferences: $sharedPreferencesName"
-            )
-        }
-
-        if (deleteEmptyPreferences && sharedPrefs.all.isEmpty()) {
-            deleteSharedPreferences(context, sharedPreferencesName)
-        }
-
-        keySet.clear()
-    }
-
-    private fun deleteSharedPreferences(context: Context, name: String) {
-        if (android.os.Build.VERSION.SDK_INT >= 24) {
-            if (!context.deleteSharedPreferences(name)) {
-                throw IOException("Unable to delete SharedPreferences: $name")
-            }
-            return
-        }
-
-        // Context.deleteSharedPreferences is SDK 24+, so we have to reproduce the definition
-        val prefsFile = getSharedPrefsFile(context, name)
-        val prefsBackup = getSharedPrefsBackup(prefsFile)
-
-        // Silently continue if we aren't able to delete the Shared Preferences File.
-        prefsFile.delete()
-        prefsBackup.delete()
-    }
-
-    // ContextImpl.getSharedPreferencesPath is private, so we have to reproduce the definition
-    private fun getSharedPrefsFile(context: Context, name: String): File {
-        val prefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
-        return File(prefsDir, "$name.xml")
-    }
-
-    // SharedPreferencesImpl.makeBackupFile is private, so we have to reproduce the definition
-    private fun getSharedPrefsBackup(prefsFile: File) = File(prefsFile.path + ".bak")
-}
+internal val MIGRATE_ALL_KEYS = null
diff --git a/datastore/datastore-core/src/test/java/androidx/datastore/DataMigrationInitializerTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/DataMigrationInitializerTest.kt
index 0729f0f..93eaf0d 100644
--- a/datastore/datastore-core/src/test/java/androidx/datastore/DataMigrationInitializerTest.kt
+++ b/datastore/datastore-core/src/test/java/androidx/datastore/DataMigrationInitializerTest.kt
@@ -54,7 +54,7 @@
         val store = newDataStore(
             initTasksList = listOf(
                 DataMigrationInitializer.getInitializer(
-                    listOf { migrateTo100 }
+                    listOf(migrateTo100)
                 )
             )
         )
@@ -70,7 +70,7 @@
         val store = newDataStore(
             initTasksList = listOf(
                 DataMigrationInitializer.getInitializer(
-                    listOf({ migratePlus2 }, { migratePlus3 })
+                    listOf(migratePlus2, migratePlus3)
                 )
             )
         )
@@ -90,7 +90,7 @@
 
         val store = newDataStore(
             initTasksList = listOf(
-                DataMigrationInitializer.getInitializer(listOf({ noOpMigration }))
+                DataMigrationInitializer.getInitializer(listOf(noOpMigration))
             )
         )
 
@@ -114,7 +114,7 @@
 
         val store = newDataStore(
             initTasksList = listOf(
-                DataMigrationInitializer.getInitializer(listOf({ noOpMigration }))
+                DataMigrationInitializer.getInitializer(listOf(noOpMigration))
             )
         )
 
@@ -140,7 +140,7 @@
         serializer.failingWrite = true
         val store = newDataStore(
             initTasksList = listOf(
-                DataMigrationInitializer.getInitializer(listOf({ noOpMigration }))
+                DataMigrationInitializer.getInitializer(listOf(noOpMigration))
             ),
             serializer = serializer
         )
@@ -162,7 +162,7 @@
 
         val store = newDataStore(
             initTasksList = listOf(
-                DataMigrationInitializer.getInitializer(listOf({ cleanUpFailingMigration }))
+                DataMigrationInitializer.getInitializer(listOf(cleanUpFailingMigration))
             )
         )
 
@@ -175,42 +175,13 @@
 
         val store = newDataStore(
             initTasksList = listOf(
-                DataMigrationInitializer.getInitializer(listOf({ neverRunMigration }))
+                DataMigrationInitializer.getInitializer(listOf(neverRunMigration))
             )
         )
 
         assertThat(store.data.first()).isEqualTo(0)
     }
 
-    @Test
-    fun testNewDataMigrationUsedOnFailure() = runBlockingTest {
-        val migrationFactory =
-            {
-                var byte: Byte = 99
-                val migration = TestingDataMigration(migration = {
-                    val unmodifiedByte = byte
-                    byte = byte.inc()
-                    unmodifiedByte
-                })
-                migration
-            }
-
-        val store = newDataStore(
-            initTasksList = listOf(
-                DataMigrationInitializer.getInitializer(listOf(migrationFactory))
-            ),
-            serializer = serializer
-        )
-
-        serializer.failingWrite = true
-
-        assertThrows<IOException> { store.data.first() }
-
-        serializer.failingWrite = false
-
-        assertThat(store.data.first()).isEqualTo(99)
-    }
-
     private fun newDataStore(
         initTasksList: List<suspend (api: InitializerApi<Byte>) -> Unit> = listOf(),
         serializer: TestingSerializer = TestingSerializer()
diff --git a/datastore/datastore-core/src/test/java/androidx/datastore/DataStoreFactoryTest.kt b/datastore/datastore-core/src/test/java/androidx/datastore/DataStoreFactoryTest.kt
index 4d17579..f58247c 100644
--- a/datastore/datastore-core/src/test/java/androidx/datastore/DataStoreFactoryTest.kt
+++ b/datastore/datastore-core/src/test/java/androidx/datastore/DataStoreFactoryTest.kt
@@ -85,26 +85,22 @@
 
         val migratedByte = 1
 
-        val migratePlus2 = {
-            object : DataMigration<Byte> {
+        val migratePlus2 = object : DataMigration<Byte> {
                 override suspend fun shouldMigrate(currentData: Byte) = true
                 override suspend fun migrate(currentData: Byte) = currentData.inc().inc()
                 override suspend fun cleanUp() {}
             }
-        }
-        val migrateMinus1 = {
-            object : DataMigration<Byte> {
+        val migrateMinus1 = object : DataMigration<Byte> {
                 override suspend fun shouldMigrate(currentData: Byte) = true
 
                 override suspend fun migrate(currentData: Byte) = currentData.dec()
 
                 override suspend fun cleanUp() {}
             }
-        }
 
         val store = factory.create(
             produceFile = { testFile },
-            migrationProducers = listOf(migratePlus2, migrateMinus1),
+            migrations = listOf(migratePlus2, migrateMinus1),
             scope = dataStoreScope,
             serializer = TestingSerializer()
         )
diff --git a/datastore/datastore-preferences/api/current.txt b/datastore/datastore-preferences/api/current.txt
index 53a9826..217726b 100644
--- a/datastore/datastore-preferences/api/current.txt
+++ b/datastore/datastore-preferences/api/current.txt
@@ -3,8 +3,8 @@
 
   public final class PreferenceDataStoreFactory {
     ctor public PreferenceDataStoreFactory();
-    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>>> migrationProducers = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
-    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>>> migrationProducers = listOf());
+    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> migrations = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
+    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> migrations = listOf());
     method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null);
     method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
   }
@@ -40,8 +40,10 @@
     method public androidx.datastore.preferences.Preferences empty();
   }
 
-  public final class SharedPreferencesToPreferencesKt {
-    method public static kotlin.jvm.functions.Function0<androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = SharedPreferencesToPreferences.MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+  public final class SharedPreferencesMigrationKt {
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS);
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName);
   }
 
 }
diff --git a/datastore/datastore-preferences/api/public_plus_experimental_current.txt b/datastore/datastore-preferences/api/public_plus_experimental_current.txt
index 53a9826..217726b 100644
--- a/datastore/datastore-preferences/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-preferences/api/public_plus_experimental_current.txt
@@ -3,8 +3,8 @@
 
   public final class PreferenceDataStoreFactory {
     ctor public PreferenceDataStoreFactory();
-    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>>> migrationProducers = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
-    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>>> migrationProducers = listOf());
+    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> migrations = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
+    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> migrations = listOf());
     method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null);
     method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
   }
@@ -40,8 +40,10 @@
     method public androidx.datastore.preferences.Preferences empty();
   }
 
-  public final class SharedPreferencesToPreferencesKt {
-    method public static kotlin.jvm.functions.Function0<androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = SharedPreferencesToPreferences.MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+  public final class SharedPreferencesMigrationKt {
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS);
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName);
   }
 
 }
diff --git a/datastore/datastore-preferences/api/restricted_current.txt b/datastore/datastore-preferences/api/restricted_current.txt
index 53a9826..217726b 100644
--- a/datastore/datastore-preferences/api/restricted_current.txt
+++ b/datastore/datastore-preferences/api/restricted_current.txt
@@ -3,8 +3,8 @@
 
   public final class PreferenceDataStoreFactory {
     ctor public PreferenceDataStoreFactory();
-    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>>> migrationProducers = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
-    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends kotlin.jvm.functions.Function0<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>>> migrationProducers = listOf());
+    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> migrations = listOf(), kotlinx.coroutines.CoroutineScope scope = CoroutineScope(Dispatchers.IO + SupervisorJob()));
+    method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null, java.util.List<? extends androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> migrations = listOf());
     method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile, androidx.datastore.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.Preferences>? corruptionHandler = null);
     method public androidx.datastore.DataStore<androidx.datastore.preferences.Preferences> create(kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
   }
@@ -40,8 +40,10 @@
     method public androidx.datastore.preferences.Preferences empty();
   }
 
-  public final class SharedPreferencesToPreferencesKt {
-    method public static kotlin.jvm.functions.Function0<androidx.datastore.DataMigration<androidx.datastore.preferences.Preferences>> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = SharedPreferencesToPreferences.MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+  public final class SharedPreferencesMigrationKt {
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS, boolean deleteEmptyPreferences = true);
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName, java.util.Set<java.lang.String>? keysToMigrate = MIGRATE_ALL_KEYS);
+    method public static androidx.datastore.migrations.SharedPreferencesMigration<androidx.datastore.preferences.Preferences> SharedPreferencesMigration(android.content.Context context, String sharedPreferencesName);
   }
 
 }
diff --git a/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt b/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
index 10f9c23..c92da6e 100644
--- a/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
+++ b/datastore/datastore-preferences/src/androidTest/java/androidx/datastore/preferences/SharedPreferencesToPreferencesTest.kt
@@ -391,11 +391,11 @@
     }
 
     private fun getDataStoreWithMigrations(
-        migrationProducers: List<() -> DataMigration<Preferences>>
+        migrations: List<DataMigration<Preferences>>
     ): DataStore<Preferences> {
         return PreferenceDataStoreFactory().create(
             produceFile = { datastoreFile },
-            migrationProducers = migrationProducers,
+            migrations = migrations,
             scope = TestCoroutineScope()
         )
     }
diff --git a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
index 5c77987..7de4135 100644
--- a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
+++ b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
@@ -43,16 +43,16 @@
      * @param corruptionHandler The corruptionHandler is invoked if DataStore encounters a [CorruptionException] when
      * attempting to read data. CorruptionExceptions are thrown by serializers when data can
      * not be de-serialized.
-     * @param migrationProducers Migrations are run before any access to data can occur. Each
+     * @param migrations are run before any access to data can occur. Each
      * producer and migration may be run more than once whether or not it already succeeded
      * (potentially because another migration failed or a write to disk failed.)
      * @param scope The scope in which IO operations and transform functions will execute.
      */
-    @JvmOverloads
+    @JvmOverloads // Generate methods for default params for java users.
     fun create(
         produceFile: () -> File,
         corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
-        migrationProducers: List<() -> DataMigration<Preferences>> = listOf(),
+        migrations: List<DataMigration<Preferences>> = listOf(),
         scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
     ): DataStore<Preferences> =
         dataStoreFactory.create(
@@ -66,7 +66,7 @@
             },
             serializer = PreferencesSerializer,
             corruptionHandler = corruptionHandler,
-            migrationProducers = migrationProducers,
+            migrations = migrations,
             scope = scope
         )
 }
\ No newline at end of file
diff --git a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt
new file mode 100644
index 0000000..0e2a46c
--- /dev/null
+++ b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesMigration.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.datastore.preferences
+
+import android.content.Context
+import androidx.datastore.migrations.SharedPreferencesView
+import androidx.datastore.migrations.SharedPreferencesMigration
+
+/**
+ * Creates a SharedPreferencesMigration for DataStore<Preferences>.
+ *
+ * @param context Context used for getting SharedPreferences.
+ * @param sharedPreferencesName The name of the SharedPreferences.
+ * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore.Preferences with
+ * their same values. If the key is already present in the new Preferences, the key
+ * will not be migrated again. If the key is not present in the SharedPreferences it
+ * will not be migrated. If keysToMigrate is not set, all keys will be migrated from the existing
+ * SharedPreferences.
+ * @param deleteEmptyPreferences If enabled and the SharedPreferences are empty (i.e. no remaining
+ * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
+ * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
+ * SharedPreferences to begin with then the (potentially) empty SharedPreferences
+ * won't be cleaned up by this option. This functionality is best effort - if there
+ * is an issue deleting the SharedPreferences file it will be silently ignored.
+ *
+ * TODO(rohitsat): determine whether to remove the deleteEmptyPreferences option.
+ */
+@JvmOverloads // Generate methods for default params for java users.
+fun SharedPreferencesMigration(
+    context: Context,
+    sharedPreferencesName: String,
+    keysToMigrate: Set<String>? = MIGRATE_ALL_KEYS,
+    deleteEmptyPreferences: Boolean = true
+): SharedPreferencesMigration<Preferences> {
+    return SharedPreferencesMigration(
+        context = context,
+        sharedPreferencesName = sharedPreferencesName,
+        keysToMigrate = keysToMigrate,
+        deleteEmptyPreferences = deleteEmptyPreferences,
+        shouldRunMigration = { prefs ->
+            // If any key hasn't been migrated to currentData, we can't skip the migration. If
+            // the key set is not specified, we can't skip the migration.
+            keysToMigrate?.any { it !in prefs } ?: true
+        },
+        migrate = { sharedPrefs: SharedPreferencesView, currentData: Preferences ->
+            // prefs.getAll is already filtered to our key set.
+            val preferencesToMigrate =
+                sharedPrefs.getAll().filter { (key, _) -> key !in currentData }
+
+            val preferencesBuilder = currentData.toBuilder()
+            for ((key, value) in preferencesToMigrate) {
+                when (value) {
+                    is Boolean -> preferencesBuilder.setBoolean(key, value)
+                    is Float -> preferencesBuilder.setFloat(key, value)
+                    is Int -> preferencesBuilder.setInt(key, value)
+                    is Long -> preferencesBuilder.setLong(key, value)
+                    is String -> preferencesBuilder.setString(key, value)
+                    is Set<*> ->
+                        @Suppress("UNCHECKED_CAST")
+                        preferencesBuilder.setStringSet(key, value.toSet() as Set<String>)
+                }
+            }
+
+            preferencesBuilder.build()
+        })
+}
+
+internal val MIGRATE_ALL_KEYS = null
\ No newline at end of file
diff --git a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesToPreferences.kt b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesToPreferences.kt
deleted file mode 100644
index 0a70ea3..0000000
--- a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/SharedPreferencesToPreferences.kt
+++ /dev/null
@@ -1,103 +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.datastore.preferences
-
-import android.content.Context
-import androidx.datastore.DataMigration
-import androidx.datastore.migrations.MigrationFromSharedPreferences
-import androidx.datastore.migrations.SharedPreferencesView
-import androidx.datastore.migrations.SharedPreferencesMigration
-
-/**
- * Creates a SharedPreferencesMigration for DataStore<Preferences>.
- *
- * @param context Context used for getting SharedPreferences.
- * @param sharedPreferencesName The name of the SharedPreferences.
- * @param keysToMigrate The list of keys to migrate. The keys will be mapped to datastore.Preferences with
- * their same values. If the key is already present in the new Preferences, the key
- * will not be migrated again. If the key is not present in the SharedPreferences it
- * will not be migrated. If keysToMigrate is not set, all keys will be migrated from the existing
- * SharedPreferences.
- * @param deleteEmptyPreferences If enabled and the SharedPreferences are empty (i.e. no remaining
- * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
- * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
- * SharedPreferences to begin with then the (potentially) empty SharedPreferences
- * won't be cleaned up by this option. This functionality is best effort - if there
- * is an issue deleting the SharedPreferences file it will be silently ignored.
- */
-fun SharedPreferencesMigration(
-    context: Context,
-    sharedPreferencesName: String,
-    keysToMigrate: Set<String>? = SharedPreferencesToPreferences.MIGRATE_ALL_KEYS,
-    deleteEmptyPreferences: Boolean = true
-): () -> DataMigration<Preferences> {
-    return SharedPreferencesMigration(
-        context,
-        sharedPreferencesName,
-        SharedPreferencesToPreferences(keysToMigrate),
-        keysToMigrate,
-        deleteEmptyPreferences
-    )
-}
-
-/**
- * A DataMigration which migrates SharedPreferences to DataStore.
- *
- * Note: this accesses the SharedPreferences using MODE_PRIVATE.
- */
-internal class SharedPreferencesToPreferences(
-    private val keysToMigrate: Set<String>?
-) : MigrationFromSharedPreferences<Preferences> {
-
-    companion object {
-        internal val MIGRATE_ALL_KEYS = null
-    }
-
-    override suspend fun shouldMigrate(currentData: Preferences): Boolean {
-        if (keysToMigrate == null) {
-            // We need to migrate all keys from the SharedPreferences.
-            return true
-        }
-
-        // If any key hasn't been migrated to currentData, we can't skip the migration.
-        return keysToMigrate.any { it !in currentData }
-    }
-
-    override suspend fun migrate(
-        prefs: SharedPreferencesView,
-        currentData: Preferences
-    ): Preferences {
-        // prefs.getAll is already filtered to our key set.
-        val preferencesToMigrate = prefs.getAll().filter { (key, _) -> key !in currentData }
-
-        val preferencesBuilder = currentData.toBuilder()
-        for ((key, value) in preferencesToMigrate) {
-            when (value) {
-                is Boolean -> preferencesBuilder.setBoolean(key, value)
-                is Float -> preferencesBuilder.setFloat(key, value)
-                is Int -> preferencesBuilder.setInt(key, value)
-                is Long -> preferencesBuilder.setLong(key, value)
-                is String -> preferencesBuilder.setString(key, value)
-                is Set<*> ->
-                    @Suppress("UNCHECKED_CAST")
-                    preferencesBuilder.setStringSet(key, value.toSet() as Set<String>)
-            }
-        }
-
-        return preferencesBuilder.build()
-    }
-}
\ No newline at end of file
diff --git a/datastore/datastore-preferences/src/test/java/androidx/datastore/preferences/PreferenceDataStoreFactoryTest.kt b/datastore/datastore-preferences/src/test/java/androidx/datastore/preferences/PreferenceDataStoreFactoryTest.kt
index e200d12..7ae14dc 100644
--- a/datastore/datastore-preferences/src/test/java/androidx/datastore/preferences/PreferenceDataStoreFactoryTest.kt
+++ b/datastore/datastore-preferences/src/test/java/androidx/datastore/preferences/PreferenceDataStoreFactoryTest.kt
@@ -91,30 +91,27 @@
             .setBoolean("boolean_key", true)
             .build()
 
-        val migrateTo5 = {
-            object : DataMigration<Preferences> {
-                override suspend fun shouldMigrate(currentData: Preferences) = true
+        val migrateTo5 = object : DataMigration<Preferences> {
+            override suspend fun shouldMigrate(currentData: Preferences) = true
 
-                override suspend fun migrate(currentData: Preferences) =
-                    currentData.toBuilder().setString("string_key", "value").build()
+            override suspend fun migrate(currentData: Preferences) =
+                currentData.toBuilder().setString("string_key", "value").build()
 
-                override suspend fun cleanUp() {}
-            }
+            override suspend fun cleanUp() {}
         }
-        val migratePlus1 = {
-            object : DataMigration<Preferences> {
-                override suspend fun shouldMigrate(currentData: Preferences) = true
 
-                override suspend fun migrate(currentData: Preferences) =
-                    currentData.toBuilder().setBoolean("boolean_key", true).build()
+        val migratePlus1 = object : DataMigration<Preferences> {
+            override suspend fun shouldMigrate(currentData: Preferences) = true
 
-                override suspend fun cleanUp() {}
-            }
+            override suspend fun migrate(currentData: Preferences) =
+                currentData.toBuilder().setBoolean("boolean_key", true).build()
+
+            override suspend fun cleanUp() {}
         }
 
         val store = factory.create(
             produceFile = { testFile },
-            migrationProducers = listOf(migrateTo5, migratePlus1),
+            migrations = listOf(migrateTo5, migratePlus1),
             scope = dataStoreScope
         )
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
index 2843ded..31abb96 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
@@ -269,9 +269,35 @@
         val fm = fc.supportFragmentManager
 
         val fragment = StrictViewFragment()
+
+        fm.beginTransaction()
+            .add(android.R.id.content, fragment)
+            .setReorderingAllowed(true)
+            .setMaxLifecycle(fragment, Lifecycle.State.INITIALIZED)
+            .commitNow()
+
+        assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+        assertThat(fragment.calledOnResume).isFalse()
+    }
+
+    @Test
+    @UiThreadTest
+    fun setMaxLifecycleInitializedAfterCreated() {
+        val viewModelStore = ViewModelStore()
+        val fc = activityRule.startupFragmentController(viewModelStore)
+
+        val fm = fc.supportFragmentManager
+
+        val fragment = StrictViewFragment()
+
+        fm.beginTransaction()
+            .add(android.R.id.content, fragment)
+            .setMaxLifecycle(fragment, Lifecycle.State.CREATED)
+            .commitNow()
+
         try {
             fm.beginTransaction()
-                .add(android.R.id.content, fragment)
                 .setMaxLifecycle(fragment, Lifecycle.State.INITIALIZED)
                 .commitNow()
             fail(
@@ -281,7 +307,10 @@
         } catch (e: IllegalArgumentException) {
             assertThat(e)
                 .hasMessageThat()
-                .contains("Cannot set maximum Lifecycle below CREATED")
+                .contains(
+                    "Cannot set maximum Lifecycle to INITIALIZED after the Fragment has been " +
+                            "created"
+                )
         }
     }
 }
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
index 3edf294..346918f 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.fragment.app
 
+import androidx.fragment.app.test.EmptyFragmentTestActivity
 import androidx.fragment.app.test.TestViewModel
 import androidx.fragment.app.test.ViewModelActivity
 import androidx.fragment.app.test.ViewModelActivity.ViewModelFragment
@@ -42,6 +43,60 @@
     }
 
     @Test
+    fun testMaxLifecycleInitializedFragment() {
+        with(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
+            withActivity {
+                val fragment = StrictFragment()
+                supportFragmentManager.beginTransaction()
+                    .setReorderingAllowed(true)
+                    .add(android.R.id.content, fragment)
+                    .setMaxLifecycle(fragment, Lifecycle.State.INITIALIZED)
+                    .commitNow()
+
+                try {
+                    fragment.viewModelStore
+                } catch (e: IllegalStateException) {
+                    assertThat(e).hasMessageThat().contains(
+                        "Calling getViewModelStore() before a Fragment " +
+                                "reaches onCreate() when using setMaxLifecycle(INITIALIZED) is " +
+                                "not supported"
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testMaxLifecycleInitializedNestedFragment() {
+        with(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
+            withActivity {
+                val fragment = StrictFragment()
+                val childFragment = StrictFragment()
+
+                supportFragmentManager.beginTransaction()
+                    .setReorderingAllowed(true)
+                    .add(android.R.id.content, fragment)
+                    .setMaxLifecycle(fragment, Lifecycle.State.INITIALIZED)
+                    .commitNow()
+
+                fragment.childFragmentManager.beginTransaction()
+                    .add(android.R.id.content, childFragment)
+                    .commitNow()
+
+                try {
+                    childFragment.viewModelStore
+                } catch (e: IllegalStateException) {
+                    assertThat(e).hasMessageThat().contains(
+                        "Calling getViewModelStore() before a Fragment " +
+                                "reaches onCreate() when using setMaxLifecycle(INITIALIZED) is " +
+                                "not supported"
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
     fun testSameActivityViewModels() {
         with(ActivityScenario.launch(ViewModelActivity::class.java)) {
             val activityModel = withActivity { activityModel }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
index b095212..4bc88fb 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
@@ -248,9 +248,9 @@
             throw new IllegalArgumentException("Cannot setMaxLifecycle for Fragment not attached to"
                     + " FragmentManager " + mManager);
         }
-        if (!state.isAtLeast(Lifecycle.State.CREATED)) {
-            throw new IllegalArgumentException("Cannot set maximum Lifecycle below "
-                    + Lifecycle.State.CREATED);
+        if (state == Lifecycle.State.INITIALIZED && fragment.mState > Fragment.INITIALIZING) {
+            throw new IllegalArgumentException("Cannot set maximum Lifecycle to " + state
+                    + " after the Fragment has been created");
         }
         return super.setMaxLifecycle(fragment, state);
     }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index e7ebc3d..c7530f64 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -397,9 +397,22 @@
         if (mFragmentManager == null) {
             throw new IllegalStateException("Can't access ViewModels from detached fragment");
         }
+        if (getMinimumMaxLifecycleState() == Lifecycle.State.INITIALIZED.ordinal()) {
+            throw new IllegalStateException("Calling getViewModelStore() before a Fragment "
+                    + "reaches onCreate() when using setMaxLifecycle(INITIALIZED) is not "
+                    + "supported");
+        }
         return mFragmentManager.getViewModelStore(this);
     }
 
+
+    private int getMinimumMaxLifecycleState() {
+        if (mMaxState == Lifecycle.State.INITIALIZED || mParentFragment == null) {
+            return mMaxState.ordinal();
+        }
+        return Math.min(mMaxState.ordinal(), mParentFragment.getMinimumMaxLifecycleState());
+    }
+
     /**
      * {@inheritDoc}
      *
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
index 13a25cf..00c1d0b 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
@@ -238,6 +238,9 @@
             case CREATED:
                 maxState = Math.min(maxState, Fragment.CREATED);
                 break;
+            case INITIALIZED:
+                maxState = Math.min(maxState, Fragment.ATTACHED);
+                break;
             default:
                 maxState = Math.min(maxState, Fragment.INITIALIZING);
         }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
index 7c6c6c72..ec4771f 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
@@ -454,9 +454,10 @@
      * already above the received state, it will be forced down to the correct state.
      *
      * <p>The fragment provided must currently be added to the FragmentManager to have it's
-     * Lifecycle state capped, or previously added as part of this transaction. The
-     * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise
-     * an {@link IllegalArgumentException} will be thrown.</p>
+     * Lifecycle state capped, or previously added as part of this transaction. If the
+     * {@link Lifecycle.State#INITIALIZED} is passed in as the {@link Lifecycle.State} and the
+     * provided fragment has already moved beyond {@link Lifecycle.State#INITIALIZED}, an
+     * {@link IllegalArgumentException} will be thrown.</p>
      *
      * @param fragment the fragment to have it's state capped.
      * @param state the ceiling state for the fragment.
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
index 9627125..382e579 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
@@ -4902,6 +4902,70 @@
     }
 
     @Test
+    public void testAccessibilityFocusOutFrontEnd_actionsAvailable() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear);
+        int[] items = new int[5];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+        final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
+                .getCompatAccessibilityDelegate();
+        final AccessibilityNodeInfoCompat info1 = AccessibilityNodeInfoCompat.obtain();
+        // Test not allowing going out both ends
+        mLayoutManager.setFocusOutAllowed(/* throughFront= */ false,
+                /* throughEnd= */ false);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info1);
+            }
+        });
+        // When not allowing jumping out both end, handle action scroll backward/forward to block
+        // it.
+        if (Build.VERSION.SDK_INT >= 21) {
+            assertTrue(hasAction(info1,
+                    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT));
+            assertTrue(hasAction(info1,
+                    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT));
+        } else {
+            assertTrue(hasAction(info1,
+                    AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD));
+            assertTrue(hasAction(info1,
+                    AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD));
+        }
+        final AccessibilityNodeInfoCompat info2 = AccessibilityNodeInfoCompat.obtain();
+        // Test allowing focus to jump out at front when reaching front.
+        mLayoutManager.setFocusOutAllowed(/* throughFront= */ true,
+                /* throughEnd= */ false);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info2);
+            }
+        });
+        // When only allowing jumping out front, block action scroll backward when reaching front
+        // for Talkback to jump focus out.
+        if (Build.VERSION.SDK_INT >= 21) {
+            assertFalse(hasAction(info2,
+                    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_LEFT));
+            assertTrue(hasAction(info2,
+                    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_RIGHT));
+        } else {
+            assertFalse(hasAction(info2,
+                    AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD));
+            assertTrue(hasAction(info2,
+                    AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD));
+        }
+    }
+
+    @Test
     public void testAccessibilitySaveContextCrash() throws Throwable {
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
index e15e2c9..3a86542 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
@@ -37,6 +37,7 @@
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AccelerateDecelerateInterpolator;
 
 import androidx.annotation.VisibleForTesting;
@@ -3758,20 +3759,40 @@
                 }
             }
         }
-        switch (translatedAction) {
-            case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
-                processPendingMovement(false);
-                processSelectionMoves(false, -1);
-                break;
-            case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
-                processPendingMovement(true);
-                processSelectionMoves(false, 1);
-                break;
+        boolean scrollingReachedBeginning = (mFocusPosition == 0
+                && translatedAction == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+        boolean scrollingReachedEnd = (mFocusPosition == state.getItemCount() - 1
+                && translatedAction == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+        if (scrollingReachedBeginning || scrollingReachedEnd) {
+            // Send a fake scroll completion event to notify Talkback that the scroll event was
+            // successful. Hence, Talkback will only look for next focus within the RecyclerView.
+            // Not sending this will result in Talkback classifying it as a failed scroll event, and
+            // will try to jump focus out of the RecyclerView.
+            // We know at this point that either focusOutFront or focusOutEnd is true (or both),
+            // because otherwise, we never hit ACTION_SCROLL_BACKWARD/FORWARD here.
+            sendTypeViewScrolledAccessibilityEvent();
+        } else {
+            switch (translatedAction) {
+                case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
+                    processPendingMovement(false);
+                    processSelectionMoves(false, -1);
+                    break;
+                case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
+                    processPendingMovement(true);
+                    processSelectionMoves(false, 1);
+                    break;
+            }
         }
         leaveContext();
         return true;
     }
 
+    private void sendTypeViewScrolledAccessibilityEvent() {
+        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+        mBaseGridView.onInitializeAccessibilityEvent(event);
+        mBaseGridView.requestSendAccessibilityEvent(mBaseGridView, event);
+    }
+
     /*
      * Move mFocusPosition multiple steps on the same row in main direction.
      * Stops when moves are all consumed or reach first/last visible item.
@@ -3826,45 +3847,58 @@
         return moves;
     }
 
+    private void addA11yActionMovingBackward(AccessibilityNodeInfoCompat info,
+            boolean reverseFlowPrimary) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            if (mOrientation == HORIZONTAL) {
+                info.addAction(reverseFlowPrimary
+                        ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
+                        .ACTION_SCROLL_RIGHT :
+                        AccessibilityNodeInfoCompat.AccessibilityActionCompat
+                                .ACTION_SCROLL_LEFT);
+            } else {
+                info.addAction(
+                        AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP);
+            }
+        } else {
+            info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+        }
+        info.setScrollable(true);
+    }
+
+    private void addA11yActionMovingForward(AccessibilityNodeInfoCompat info,
+            boolean reverseFlowPrimary) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            if (mOrientation == HORIZONTAL) {
+                info.addAction(reverseFlowPrimary
+                        ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
+                        .ACTION_SCROLL_LEFT :
+                        AccessibilityNodeInfoCompat.AccessibilityActionCompat
+                                .ACTION_SCROLL_RIGHT);
+            } else {
+                info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat
+                        .ACTION_SCROLL_DOWN);
+            }
+        } else {
+            info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+        }
+        info.setScrollable(true);
+    }
+
     @Override
     public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
             AccessibilityNodeInfoCompat info) {
         saveContext(recycler, state);
         int count = state.getItemCount();
+        // reverseFlowPrimary is whether we are in LTR/RTL mode.
         boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0;
-        if (count > 1 && !isItemFullyVisible(0)) {
-            if (Build.VERSION.SDK_INT >= 23) {
-                if (mOrientation == HORIZONTAL) {
-                    info.addAction(reverseFlowPrimary
-                            ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
-                                    .ACTION_SCROLL_RIGHT :
-                            AccessibilityNodeInfoCompat.AccessibilityActionCompat
-                                    .ACTION_SCROLL_LEFT);
-                } else {
-                    info.addAction(
-                            AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP);
-                }
-            } else {
-                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
-            }
-            info.setScrollable(true);
+        // If focusOutFront/focusOutEnd is false, override Talkback in handling
+        // backward/forward actions by adding such actions to supported action list.
+        if ((mFlag & PF_FOCUS_OUT_FRONT) == 0 || (count > 1 && !isItemFullyVisible(0))) {
+            addA11yActionMovingBackward(info, reverseFlowPrimary);
         }
-        if (count > 1 && !isItemFullyVisible(count - 1)) {
-            if (Build.VERSION.SDK_INT >= 23) {
-                if (mOrientation == HORIZONTAL) {
-                    info.addAction(reverseFlowPrimary
-                            ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
-                                    .ACTION_SCROLL_LEFT :
-                            AccessibilityNodeInfoCompat.AccessibilityActionCompat
-                                    .ACTION_SCROLL_RIGHT);
-                } else {
-                    info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat
-                                    .ACTION_SCROLL_DOWN);
-                }
-            } else {
-                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
-            }
-            info.setScrollable(true);
+        if ((mFlag & PF_FOCUS_OUT_END) == 0 || (count > 1 && !isItemFullyVisible(count - 1))) {
+            addA11yActionMovingForward(info, reverseFlowPrimary);
         }
         final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
                 AccessibilityNodeInfoCompat.CollectionInfoCompat
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
index c4e8d90..73fa138 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
@@ -202,7 +202,7 @@
             notifyRequestFailed(requestId, REASON_INVALID_COMMAND);
             return;
         }
-        sessionRecord.release();
+        sessionRecord.release(/*shouldUnselect=*/true);
     }
 
     @Override
@@ -502,7 +502,7 @@
             sessionRecord = mSessionRecords.remove(sessionId);
         }
         if (sessionRecord != null) {
-            sessionRecord.release();
+            sessionRecord.release(/*shouldUnselect=*/false);
         }
     }
 
@@ -747,7 +747,7 @@
             }
         }
 
-        public void release() {
+        public void release(boolean shouldUnselect) {
             if (!mIsReleased) {
                 // Release member controllers
                 if ((mFlags & (SESSION_FLAG_MR2 | SESSION_FLAG_GROUP))
@@ -755,7 +755,7 @@
                     updateMemberRouteControllers(null, mSessionInfo, null);
                 }
 
-                if ((mFlags & SESSION_FLAG_MR2) != 0) {
+                if (shouldUnselect) {
                     mController.onUnselect(MediaRouter.UNSELECT_REASON_STOPPED);
                     mController.onRelease();
                 }
diff --git a/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/BandSelectionHelperTest.java b/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
index 3a9a886..59a831f 100644
--- a/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
+++ b/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
@@ -82,7 +82,7 @@
             }
         });
 
-        FocusDelegate<String> focusDelegate = FocusDelegate.dummy();
+        FocusDelegate<String> focusDelegate = FocusDelegate.stub();
 
         mBandController = new BandSelectionHelper<String>(
                 mHostEnv,
diff --git a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventRouter.java b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventRouter.java
index c2a454c..542f25d 100644
--- a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventRouter.java
+++ b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventRouter.java
@@ -39,7 +39,7 @@
     private boolean mDisallowIntercept;
 
     EventRouter() {
-        mDelegates = new ToolHandlerRegistry<>(new DummyOnItemTouchListener());
+        mDelegates = new ToolHandlerRegistry<>(new StubOnItemTouchListener());
     }
 
     /**
diff --git a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusDelegate.java b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusDelegate.java
index 9bc31ec..80bfb38 100644
--- a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusDelegate.java
+++ b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusDelegate.java
@@ -28,7 +28,7 @@
  */
 public abstract class FocusDelegate<K> {
 
-    static <K> FocusDelegate<K> dummy() {
+    static <K> FocusDelegate<K> stub() {
         return new FocusDelegate<K>() {
             @Override
             public void focusItem(@NonNull ItemDetails<K> item) {
diff --git a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java
index ef0f077..95522a2 100644
--- a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java
+++ b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java
@@ -50,7 +50,7 @@
         if (delegate != null) {
             mDelegate = delegate;
         } else {
-            mDelegate = new DummyOnItemTouchListener();
+            mDelegate = new StubOnItemTouchListener();
         }
     }
 
diff --git a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionTracker.java b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
index 3e6a4ba..89df922 100644
--- a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
+++ b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
@@ -501,7 +501,7 @@
         private ItemKeyProvider<K> mKeyProvider;
         private ItemDetailsLookup<K> mDetailsLookup;
 
-        private FocusDelegate<K> mFocusDelegate = FocusDelegate.dummy();
+        private FocusDelegate<K> mFocusDelegate = FocusDelegate.stub();
 
         private OnItemActivatedListener<K> mOnItemActivatedListener;
         private OnDragInitiatedListener mOnDragInitiatedListener;
@@ -783,7 +783,7 @@
             // be configured to handle other types of input (to satisfy user expectation).);
 
             // Internally, the code doesn't permit nullable listeners, so we lazily
-            // initialize dummy instances if the developer didn't supply a real listener.
+            // initialize stub instances if the developer didn't supply a real listener.
             mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
                     ? mOnDragInitiatedListener
                     : new OnDragInitiatedListener() {
diff --git a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StubOnItemTouchListener.java
similarity index 93%
rename from recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java
rename to recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StubOnItemTouchListener.java
index 5880d97..ce3262c 100644
--- a/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java
+++ b/recyclerview/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StubOnItemTouchListener.java
@@ -25,7 +25,7 @@
  * No-op implementation of OnItemTouchListener suitable for use as a default
  * handler w/ ToolHandlerRegistery, or in tests.
  */
-final class DummyOnItemTouchListener implements RecyclerView.OnItemTouchListener {
+final class StubOnItemTouchListener implements RecyclerView.OnItemTouchListener {
     @Override
     public boolean onInterceptTouchEvent(
             @NonNull RecyclerView unused, @NonNull MotionEvent e) {
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoAdapter.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoAdapter.java
index 94be45d..94131b5 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoAdapter.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoAdapter.java
@@ -20,18 +20,16 @@
 
 import android.content.Context;
 import android.net.Uri;
-import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.util.Predicate;
 import androidx.recyclerview.selection.ItemKeyProvider;
 import androidx.recyclerview.selection.SelectionTracker;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.example.android.supportv7.Cheeses;
-import com.example.android.supportv7.R;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -47,13 +45,15 @@
     // Our list of thingies. Our DemoHolder subclasses extract display
     // values directly from the Uri, so we only need this simple list.
     // The list also contains entries for alphabetical section headers.
-    private final List<Uri> mCheeses;
+    private final List<Uri> mCheeses = new ArrayList<>();
+    private boolean mSmallItemLayout;
+    private boolean mAllCheesesEnabled;
 
     // This default implementation must be replaced
     // with a real implementation in #bindSelectionHelper.
-    private SelectionTest mSelTest = new SelectionTest() {
+    private Predicate<Uri> mIsSelectedTest = new Predicate<Uri>() {
         @Override
-        public boolean isSelected(Uri id) {
+        public boolean test(Uri key) {
             throw new IllegalStateException(
                     "Adapter must be initialized with SelectionTracker");
         }
@@ -61,7 +61,6 @@
 
     DemoAdapter(Context context) {
         mContext = context;
-        mCheeses = createCheeseList("CheeseKindom");
         mKeyProvider = new KeyProvider(mCheeses);
 
         // In the fancy edition of selection support we supply access to stable
@@ -77,22 +76,14 @@
     // Glue together SelectionTracker and the adapter.
     public void bindSelectionTracker(final SelectionTracker<Uri> tracker) {
         checkArgument(tracker != null);
-        mSelTest = new SelectionTest() {
+        mIsSelectedTest = new Predicate<Uri>() {
             @Override
-            public boolean isSelected(Uri id) {
-                return tracker.isSelected(id);
+            public boolean test(Uri key) {
+                return tracker.isSelected(key);
             }
         };
     }
 
-    void loadData() {
-        onDataReady();
-    }
-
-    private void onDataReady() {
-        notifyDataSetChanged();
-    }
-
     @Override
     public int getItemCount() {
         return mCheeses.size();
@@ -105,22 +96,21 @@
 
     @Override
     public void onBindViewHolder(@NonNull DemoHolder holder, int position) {
-        if (holder instanceof DemoHeaderHolder) {
-            Uri uri = mKeyProvider.getKey(position);
-            ((DemoHeaderHolder) holder).update(uri.getPathSegments().get(0));
-        } else if (holder instanceof DemoItemHolder) {
-            Uri uri = mKeyProvider.getKey(position);
-            ((DemoItemHolder) holder).update(uri, uri.getPathSegments().get(1),
-                    mSelTest.isSelected(uri));
+        Uri uri = mKeyProvider.getKey(position);
+        holder.update(uri);
+        if (holder instanceof DemoItemHolder) {
+            DemoItemHolder itemHolder = (DemoItemHolder) holder;
+            itemHolder.setSelected(mIsSelectedTest.test(uri));
+            itemHolder.setSmallLayoutMode(mSmallItemLayout);
         }
     }
 
     @Override
     public int getItemViewType(int position) {
-        Uri key = mKeyProvider.getKey(position);
-        if (key.getPathSegments().size() == 1) {
+        Uri uri = mKeyProvider.getKey(position);
+        if (Uris.isGroup(uri)) {
             return TYPE_HEADER;
-        } else if (key.getPathSegments().size() == 2) {
+        } else if (Uris.isCheese(uri)) {
             return TYPE_ITEM;
         }
 
@@ -131,57 +121,32 @@
     public DemoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
         switch (viewType) {
             case TYPE_HEADER:
-                return new DemoHeaderHolder(
-                        inflateLayout(mContext, parent, R.layout.selection_demo_list_header));
+                return new DemoHeaderHolder(mContext, parent);
             case TYPE_ITEM:
-                return new DemoItemHolder(
-                        inflateLayout(mContext, parent, R.layout.selection_demo_list_item));
+                return new DemoItemHolder(mContext, parent);
         }
         throw new RuntimeException("Unsupported view type" + viewType);
     }
 
-    @SuppressWarnings("TypeParameterUnusedInFormals")  // Convenience to avoid clumsy cast.
-    private static <V extends View> V inflateLayout(
-            Context context, ViewGroup parent, int layout) {
-
-        return (V) LayoutInflater.from(context).inflate(layout, parent, false);
-    }
-
     // Creates a list of cheese Uris and section header Uris.
-    private static List<Uri> createCheeseList(String authority) {
-        List<Uri> cheeses = new ArrayList<>();
-        char section = '-';  // any ol' value other than 'a' will do the trick here.
+    private void populateCheeses(int maxItemsPerGroup) {
+        String group = "-";  // any ol' value other than 'a' will do the trick here.
+        int itemsInGroup = 0;
 
         for (String cheese : Cheeses.sCheeseStrings) {
-            char leadingChar = cheese.toLowerCase().charAt(0);
+            String leadingChar = Character.toString(cheese.toLowerCase().charAt(0));
 
             // When we find a new leading character insert an artificial
             // cheese header
-            if (leadingChar != section) {
-                section = leadingChar;
-                Uri headerUri = new Uri.Builder()
-                        .scheme("content")
-                        .encodedAuthority(authority)
-                        .appendPath(Character.toString(section))
-                        .build();
-
-                cheeses.add(headerUri);
+            if (!leadingChar.equals(group)) {
+                group = leadingChar;
+                itemsInGroup = 0;
+                mCheeses.add(Uris.forGroup(group));
             }
-
-            Uri itemUri = new Uri.Builder()
-                    .scheme("content")
-                    .encodedAuthority(authority)
-                    .appendPath(Character.toString(section))
-                    .appendPath(cheese)
-                    .build();
-            cheeses.add(itemUri);
+            if (++itemsInGroup <= maxItemsPerGroup) {
+                mCheeses.add(Uris.forCheese(group, cheese));
+            }
         }
-
-        return cheeses;
-    }
-
-    private interface SelectionTest {
-        boolean isSelected(Uri id);
     }
 
     public boolean removeItem(Uri key) {
@@ -195,6 +160,30 @@
         return removed != null;
     }
 
+    void enableSmallItemLayout(boolean enabled) {
+        mSmallItemLayout = enabled;
+    }
+
+    void enableAllCheeses(boolean enabled) {
+        mAllCheesesEnabled = enabled;
+    }
+
+    boolean smallItemLayoutEnabled() {
+        return mSmallItemLayout;
+    }
+
+    boolean allCheesesEnabled() {
+        return mAllCheesesEnabled;
+    }
+
+
+
+    void refresh() {
+        mCheeses.clear();
+        populateCheeses(mAllCheesesEnabled ? Integer.MAX_VALUE : 5);
+        notifyDataSetChanged();
+    }
+
     /**
      * When ever possible provide the selection library with a
      * "SCOPED_MAPPED" ItemKeyProvider. This enables the selection
@@ -211,13 +200,13 @@
 
         private final List<Uri> mData;
 
-        KeyProvider(List<Uri> cheeses) {
+        KeyProvider(List<Uri> data) {
             // Advise the world we can supply ids/position for any item at any time,
             // not just when visible in RecyclerView.
             // This enables fancy stuff especially helpful to users with pointy
             // devices like Chromebooks, or tablets with touch pads
             super(SCOPE_MAPPED);
-            mData = cheeses;
+            mData = data;
         }
 
         @Override
@@ -228,13 +217,7 @@
         @Override
         public int getPosition(@NonNull Uri key) {
             int position = Collections.binarySearch(mData, key);
-            // position is insertion point if key is missing.
-            // Since the insertion point could be end of the list + 1
-            // both verify the position is in bounds, and that the value
-            // at position is the same as the key.
-            return position >= 0 && position <= mData.size() - 1 && key.equals(mData.get(position))
-                    ? position
-                    : RecyclerView.NO_POSITION;
+            return position >= 0 ? position : RecyclerView.NO_POSITION;
         }
     }
 }
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHeaderHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHeaderHolder.java
index be78dd0..0e64018 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHeaderHolder.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHeaderHolder.java
@@ -15,26 +15,32 @@
  */
 package com.example.android.supportv7.widget.selection.fancy;
 
-import android.view.View;
+import android.content.Context;
+import android.net.Uri;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
 
 import com.example.android.supportv7.R;
 
 final class DemoHeaderHolder extends DemoHolder {
 
-    private static final String HEADER_TAG = "I'm a header";
     final TextView mLabel;
 
-    DemoHeaderHolder(LinearLayout layout) {
+    DemoHeaderHolder(@NonNull Context context, @NonNull ViewGroup parent) {
+        this(inflateLayout(context, parent, R.layout.selection_demo_list_header));
+    }
+
+    private DemoHeaderHolder(LinearLayout layout) {
         super(layout);
-        layout.setTag(HEADER_TAG);
         mLabel = layout.findViewById(R.id.label);
     }
 
-    void update(String label) {
+    @Override
+    void update(@NonNull Uri uri) {
+        String label = Uris.getGroup(uri);
         mLabel.setText(label.toUpperCase() + label + label + "...");
     }
 
@@ -42,8 +48,4 @@
     public String toString() {
         return "Header{name:" + mLabel.getText() + "}";
     }
-
-    static boolean isHeader(@Nullable View view) {
-        return view == null ? false : HEADER_TAG.equals(view.getTag());
-    }
 }
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHolder.java
index 0c6b999..fd8d494 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHolder.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoHolder.java
@@ -16,12 +16,25 @@
 
 package com.example.android.supportv7.widget.selection.fancy;
 
+import android.content.Context;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
 
 abstract class DemoHolder extends RecyclerView.ViewHolder {
     DemoHolder(LinearLayout layout) {
         super(layout);
     }
+
+    abstract void update(@NonNull Uri uri);
+
+    @SuppressWarnings("TypeParameterUnusedInFormals")  // Convenience to avoid clumsy cast.
+    static <V extends View> V inflateLayout(Context context, ViewGroup parent, int layout) {
+        return (V) LayoutInflater.from(context).inflate(layout, parent, false);
+    }
 }
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoItemHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoItemHolder.java
index 176fb3d..88e932e 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoItemHolder.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/DemoItemHolder.java
@@ -15,12 +15,16 @@
  */
 package com.example.android.supportv7.widget.selection.fancy;
 
+import android.content.Context;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.Dimension;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
@@ -36,7 +40,11 @@
 
     private @Nullable Uri mKey;
 
-    DemoItemHolder(LinearLayout layout) {
+    DemoItemHolder(@NonNull Context context, @NonNull ViewGroup parent) {
+        this(inflateLayout(context, parent, R.layout.selection_demo_list_item));
+    }
+
+    private DemoItemHolder(LinearLayout layout) {
         super(layout);
 
         mContainer = layout.findViewById(R.id.container);
@@ -71,13 +79,18 @@
         };
     }
 
-    void update(Uri key, String label, boolean selected) {
-        mKey = key;
-        mLabel.setText(label);
-        setSelected(selected);
+    @Override
+    void update(@NonNull Uri uri) {
+        mKey = uri;
+        mLabel.setText(Uris.getCheese(uri));
     }
 
-    private void setSelected(boolean selected) {
+    void setSmallLayoutMode(boolean small) {
+        mSelector.setVisibility(small ? View.GONE : View.VISIBLE);
+        mLabel.setTextSize(Dimension.SP, small ? 14f : 20f);
+    }
+
+    void setSelected(boolean selected) {
         mContainer.setActivated(selected);
         mSelector.setActivated(selected);
     }
@@ -108,8 +121,8 @@
 
     boolean inSelectRegion(MotionEvent e) {
         Rect iconRect = new Rect();
-        mSelector.getGlobalVisibleRect(iconRect);
-        return iconRect.contains((int) e.getRawX(), (int) e.getRawY());
+        return mSelector.getGlobalVisibleRect(iconRect)
+                && iconRect.contains((int) e.getRawX(), (int) e.getRawY());
     }
 
     ItemDetails<Uri> getItemDetails() {
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
index 5da1896..3be459a 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
@@ -72,7 +72,6 @@
     private SelectionTracker<Uri> mSelectionTracker;
 
     private GridLayoutManager mLayout;
-    private boolean mIterceptListenerEnabled = false;
     private boolean mSwipeDuringSelectionEnabled = false;
 
     @Override
@@ -82,12 +81,23 @@
         setContentView(R.layout.selection_demo_layout);
         mRecView = (RecyclerView) findViewById(R.id.list);
 
-        // Demo how to intercept touch events before selection tracker.
-        // In case you need to do something fancy that selection tracker
-        // might otherwise interfere with.
-        setupCustomTouchListener();
-
         mLayout = new GridLayoutManager(this, 1);
+
+        // Let our headers span any number of columns.
+        mLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                switch(mAdapter.getItemViewType(position)){
+                    case DemoAdapter.TYPE_HEADER:
+                        return mLayout.getSpanCount();
+
+                    case DemoAdapter.TYPE_ITEM:
+                    default:
+                        return 1;
+                }
+            }
+        });
+
         mRecView.setLayoutManager(mLayout);
         mAdapter = new DemoAdapter(this);
         mRecView.setAdapter(mAdapter);
@@ -209,42 +219,31 @@
                 });
     }
 
-    // If you want to provided special handling of clicks on items
-    // in RecyclerView (respond to a play button, or show a menu
-    // when a three-dot menu is clicked) you can't just add an OnClickListener
-    // to the View.  This is because Selection lib installs an
-    // OnItemTouchListener w/ RecyclerView, and that listener eats
-    // up many of the touch/mouse events RecyclerView sends its way.
-    // To work around this install your own OnItemTouchListener *before*
-    // you build your SelectionTracker instance. That'll give your listener
-    // a chance to intercept events before Selection lib gobbles them up.
-    private void setupCustomTouchListener() {
-        mRecView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
-            @Override
-            public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
-                return mIterceptListenerEnabled
-                        && DemoHeaderHolder.isHeader(rv.findChildViewUnder(e.getX(), e.getY()));
-            }
-
-            @Override
-            public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
-                toast(FancySelectionDemoActivity.this, "Clicked on a header!");
-            }
-
-            @Override
-            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-            }
-        });
-    }
-
     @Override
     protected void onSaveInstanceState(@NonNull Bundle state) {
         super.onSaveInstanceState(state);
         mSelectionTracker.onSaveInstanceState(state);
+        state.putBoolean("showAll", mAdapter.allCheesesEnabled());
+        state.putBoolean("gridLayout", mAdapter.smallItemLayoutEnabled());
+        state.putBoolean("enableSwipe", mSwipeDuringSelectionEnabled);
     }
 
-    private void updateFromSavedState(Bundle state) {
+    private void updateFromSavedState(@Nullable Bundle state) {
         mSelectionTracker.onRestoreInstanceState(state);
+
+        boolean showAll = false;
+        boolean gridLayout = false;
+        if (state == null) {
+            mSwipeDuringSelectionEnabled = true;
+        } else {
+            showAll = state.getBoolean("showAll");
+            gridLayout = state.getBoolean("gridLayout");
+            mSwipeDuringSelectionEnabled = state.getBoolean("enableSwipe");
+        }
+
+        mAdapter.enableAllCheeses(showAll);
+        mLayout.setSpanCount(gridLayout ? 2 : 1);
+        mAdapter.enableSmallItemLayout(gridLayout);
     }
 
     @Override
@@ -252,22 +251,41 @@
         boolean showMenu = super.onCreateOptionsMenu(menu);
         getMenuInflater().inflate(R.menu.selection_demo_actions, menu);
         for (int i = 0; i < menu.size(); i++) {
-            updateOptionFromMenu(menu.getItem(i));
+            MenuItem item = menu.getItem(i);
+            switch (item.getItemId()) {
+                case R.id.option_menu_more_cheese:
+                    item.setChecked(mAdapter.allCheesesEnabled());
+                    break;
+                case R.id.option_menu_grid_layout:
+                    item.setChecked(mAdapter.smallItemLayoutEnabled());
+                    break;
+                case R.id.option_menu_swipe_during_select:
+                    item.setChecked(mSwipeDuringSelectionEnabled);
+                    break;
+            }
         }
         return showMenu;
     }
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        item.setChecked(!item.isChecked());
+        if (item.isCheckable()) {
+            item.setChecked(!item.isChecked());
+        }
         updateOptionFromMenu(item);
         return true;
     }
 
     private void updateOptionFromMenu(@NonNull MenuItem item) {
         switch (item.getItemId()) {
-            case R.id.option_menu_custom_listener:
-                mIterceptListenerEnabled = item.isChecked();
+            case R.id.option_menu_more_cheese:
+                mAdapter.enableAllCheeses(item.isChecked());
+                mAdapter.refresh();
+                break;
+            case R.id.option_menu_grid_layout:
+                mAdapter.enableSmallItemLayout(item.isChecked());
+                mLayout.setSpanCount(item.isChecked() ? 2 : 1);
+                mAdapter.refresh();
                 break;
             case R.id.option_menu_swipe_during_select:
                 mSwipeDuringSelectionEnabled = item.isChecked();
@@ -329,7 +347,7 @@
     @Override
     protected void onStart() {
         super.onStart();
-        mAdapter.loadData();
+        mAdapter.refresh();
     }
 
     // Tracking focus separately from explicit selection
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/Uris.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/Uris.java
new file mode 100644
index 0000000..ca9cd57
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/Uris.java
@@ -0,0 +1,69 @@
+/*
+ * 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 com.example.android.supportv7.widget.selection.fancy;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+final class Uris {
+
+    private Uris() {}
+
+    static final String SCHEME = "content";
+    static final String AUTHORITY = "CheeseWorld";
+    static final String PARAM_GROUP = "g";
+    static final String PARAM_CHEESE = "c";
+
+    static @NonNull Uri forGroup(@NonNull String group) {
+        return new Uri.Builder()
+                .scheme(SCHEME)
+                .encodedAuthority(AUTHORITY)
+                .appendQueryParameter(PARAM_GROUP, group)
+                .build();
+    }
+
+    static @NonNull Uri forCheese(@NonNull String group, @NonNull String cheese) {
+        return new Uri.Builder()
+                .scheme(SCHEME)
+                .encodedAuthority(AUTHORITY)
+                .appendQueryParameter(PARAM_GROUP, group)
+                .appendQueryParameter(PARAM_CHEESE, cheese)
+                .build();
+    }
+
+    static boolean isGroup(@NonNull Uri uri) {
+        return !isCheese(uri);
+    }
+
+    static boolean isCheese(@NonNull Uri uri) {
+        return uri.getQueryParameter(PARAM_GROUP) != null
+                && uri.getQueryParameter(PARAM_CHEESE) != null;
+    }
+
+    static @NonNull String getGroup(@NonNull Uri uri) {
+        String group = uri.getQueryParameter(PARAM_GROUP);
+        Preconditions.checkArgument(group != null);
+        return group;
+    }
+
+    static @NonNull String getCheese(@NonNull Uri uri) {
+        Preconditions.checkArgument(isCheese(uri));
+        return uri.getQueryParameter(PARAM_CHEESE);
+    }
+}
diff --git a/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml b/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml
index c800127..3e6959d 100644
--- a/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml
+++ b/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml
@@ -17,8 +17,8 @@
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item
         android:state_activated="false"
-        android:color="?android:attr/colorForeground"
-        android:alpha=".3"
+        android:color="@android:color/black"
+        android:alpha=".1"
         />
     <item
         android:state_activated="true"
diff --git a/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml b/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
index 3fc1f40..27e08bf 100644
--- a/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
+++ b/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
@@ -55,7 +55,9 @@
                 android:paddingEnd="0dp"
                 android:paddingStart="0dp"
                 android:paddingTop="5dp"
-                android:scrollbars="none" />
+                android:background="#11000000"
+                android:scrollbarStyle="insideOverlay"
+                android:scrollbars="vertical" />
 
         </FrameLayout>
 
diff --git a/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml b/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
index fb5e8e9..e28e922 100644
--- a/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
+++ b/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
@@ -40,8 +40,8 @@
       </TextView>
       <TextView
           android:id="@+id/label"
-          android:textSize="20sp"
-          android:textStyle="bold"
+          android:textSize="18sp"
+          android:textColor="@android:color/black"
           android:gravity="center_vertical"
           android:paddingStart="10dp"
           android:paddingEnd="10dp"
diff --git a/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml b/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml
index 17ea335..7751f3e 100644
--- a/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml
+++ b/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml
@@ -16,13 +16,15 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
-       android:id="@+id/option_menu_swipe_during_select"
-       android:title="Swipe When Selection Is Active"
-       android:checkable="true"
-       android:checked="true"/>
+       android:id="@+id/option_menu_more_cheese"
+       android:title="Show all the cheeses!"
+       android:checkable="true"/>
    <item
-       android:id="@+id/option_menu_custom_listener"
-       android:title="Custom OnItemTouchListener"
-       android:checkable="true"
-       android:checked="false"/>
+       android:id="@+id/option_menu_grid_layout"
+       android:title="Grid layout please!"
+       android:checkable="true"/>
+   <item
+       android:id="@+id/option_menu_swipe_during_select"
+       android:title="Swipe when stuff is selected!"
+       android:checkable="true"/>
 </menu>
diff --git a/ui/integration-tests/benchmark/build.gradle b/ui/integration-tests/benchmark/build.gradle
index 5e992e1..40add9b 100644
--- a/ui/integration-tests/benchmark/build.gradle
+++ b/ui/integration-tests/benchmark/build.gradle
@@ -41,6 +41,7 @@
     implementation(KOTLIN_REFLECT)
     implementation(ANDROIDX_TEST_RULES)
     implementation(JUNIT)
+    implementation(TRUTH)
 
     androidTestImplementation project(":compose:ui:ui")
     androidTestImplementation project(":compose:foundation:foundation-layout")
diff --git a/ui/integration-tests/benchmark/src/androidTest/AndroidManifest.xml b/ui/integration-tests/benchmark/src/androidTest/AndroidManifest.xml
index 1983f55..d23850c 100644
--- a/ui/integration-tests/benchmark/src/androidTest/AndroidManifest.xml
+++ b/ui/integration-tests/benchmark/src/androidTest/AndroidManifest.xml
@@ -29,5 +29,6 @@
         <!-- enable profileableByShell for non-intrusive profiling tools -->
         <!--suppress AndroidElementNotAllowed -->
         <profileable android:shell="true"/>
+        <activity android:name="androidx.ui.pointerinput.TestActivity" />
     </application>
 </manifest>
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextBasicBenchmark.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextBasicBenchmark.kt
index f228672..1b94abd 100644
--- a/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextBasicBenchmark.kt
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextBasicBenchmark.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui
 
+import androidx.compose.ui.text.AnnotatedString
 import androidx.test.filters.LargeTest
 import androidx.ui.benchmark.ComposeBenchmarkRule
 import androidx.ui.benchmark.benchmarkDrawPerf
@@ -24,7 +25,6 @@
 import androidx.ui.benchmark.toggleStateBenchmarkLayout
 import androidx.ui.benchmark.toggleStateBenchmarkMeasure
 import androidx.ui.benchmark.toggleStateBenchmarkRecompose
-import androidx.ui.integration.test.core.text.TextBasicTestCase
 import androidx.ui.integration.test.TextBenchmarkTestRule
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
@@ -32,6 +32,7 @@
 import androidx.ui.benchmark.benchmarkFirstDrawFast
 import androidx.ui.benchmark.benchmarkFirstLayoutFast
 import androidx.ui.benchmark.benchmarkFirstMeasureFast
+import androidx.ui.integration.test.core.text.TextInColumnTestCase
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -60,7 +61,7 @@
     private val width = textBenchmarkRule.widthDp.dp
     private val fontSize = textBenchmarkRule.fontSizeSp.sp
 
-    private val textCaseFactory = {
+    private val caseFactory = {
         textBenchmarkRule.generator { textGenerator ->
             /**
              * Text render has a word cache in the underlying system. To get a proper metric of its
@@ -68,9 +69,11 @@
              * public API. Here is a workaround which generates a new string when a new test case
              * is created.
              */
-            val text = textGenerator.nextParagraph(textLength)
-            TextBasicTestCase(
-                text = text,
+            val texts = List(textBenchmarkRule.repeatTimes) {
+                AnnotatedString(textGenerator.nextParagraph(textLength))
+            }
+            TextInColumnTestCase(
+                texts = texts,
                 width = width,
                 fontSize = fontSize
             )
@@ -83,7 +86,7 @@
      */
     @Test
     fun first_compose() {
-        benchmarkRule.benchmarkFirstComposeFast(textCaseFactory)
+        benchmarkRule.benchmarkFirstComposeFast(caseFactory)
     }
 
     /**
@@ -92,11 +95,7 @@
      */
     @Test
     fun first_measure() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstMeasureFast {
-                TextBasicTestCase(textGenerator.nextParagraph(textLength), width, fontSize)
-            }
-        }
+        benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
     }
 
     /**
@@ -105,7 +104,7 @@
      */
     @Test
     fun first_layout() {
-        benchmarkRule.benchmarkFirstLayoutFast(textCaseFactory)
+        benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
     }
 
     /**
@@ -113,7 +112,7 @@
      */
     @Test
     fun first_draw() {
-        benchmarkRule.benchmarkFirstDrawFast(textCaseFactory)
+        benchmarkRule.benchmarkFirstDrawFast(caseFactory)
     }
 
     /**
@@ -122,7 +121,7 @@
      */
     @Test
     fun layout() {
-        benchmarkRule.benchmarkLayoutPerf(textCaseFactory)
+        benchmarkRule.benchmarkLayoutPerf(caseFactory)
     }
 
     /**
@@ -130,7 +129,7 @@
      */
     @Test
     fun draw() {
-        benchmarkRule.benchmarkDrawPerf(textCaseFactory)
+        benchmarkRule.benchmarkDrawPerf(caseFactory)
     }
 
     /**
@@ -138,7 +137,7 @@
      */
     @Test
     fun toggleColor_recompose() {
-        benchmarkRule.toggleStateBenchmarkRecompose(textCaseFactory)
+        benchmarkRule.toggleStateBenchmarkRecompose(caseFactory)
     }
 
     /**
@@ -146,7 +145,7 @@
      */
     @Test
     fun toggleColor_measure() {
-        benchmarkRule.toggleStateBenchmarkMeasure(textCaseFactory)
+        benchmarkRule.toggleStateBenchmarkMeasure(caseFactory)
     }
 
     /**
@@ -154,7 +153,7 @@
      */
     @Test
     fun toggleColor_layout() {
-        benchmarkRule.toggleStateBenchmarkLayout(textCaseFactory)
+        benchmarkRule.toggleStateBenchmarkLayout(caseFactory)
     }
 
     /**
@@ -162,6 +161,6 @@
      */
     @Test
     fun toggleColor_draw() {
-        benchmarkRule.toggleStateBenchmarkDraw(textCaseFactory)
+        benchmarkRule.toggleStateBenchmarkDraw(caseFactory)
     }
 }
\ No newline at end of file
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextMultiStyleBenchmark.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextMultiStyleBenchmark.kt
index 073c8d7..dccc21a34 100644
--- a/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextMultiStyleBenchmark.kt
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextMultiStyleBenchmark.kt
@@ -24,11 +24,11 @@
 import androidx.ui.benchmark.benchmarkFirstLayout
 import androidx.ui.benchmark.benchmarkFirstMeasure
 import androidx.ui.benchmark.benchmarkLayoutPerf
-import androidx.ui.integration.test.core.text.TextMultiStyleTestCase
 import androidx.ui.integration.test.TextBenchmarkTestRule
 import androidx.ui.integration.test.cartesian
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.ui.integration.test.core.text.TextInColumnTestCase
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -59,25 +59,39 @@
     @get:Rule
     val benchmarkRule = ComposeBenchmarkRule()
 
-    val width = textBenchmarkRule.widthDp.dp
-    val fontSize = textBenchmarkRule.fontSizeSp.sp
+    private val width = textBenchmarkRule.widthDp.dp
+    private val fontSize = textBenchmarkRule.fontSizeSp.sp
+
+    private val caseFactory = {
+        textBenchmarkRule.generator { textGenerator ->
+            /**
+             * Text render has a word cache in the underlying system. To get a proper metric of its
+             * performance, the cache needs to be disabled, which unfortunately is not doable via
+             * public API. Here is a workaround which generates a new string when a new test case
+             * is created.
+             */
+            val texts = List(textBenchmarkRule.repeatTimes) {
+                textGenerator.nextAnnotatedString(
+                    length = textLength,
+                    styleCount = styleCount,
+                    hasMetricAffectingStyle = true
+                )
+            }
+            TextInColumnTestCase(
+                texts = texts,
+                width = width,
+                fontSize = fontSize
+            )
+        }
+    }
+
     /**
      * Measure the time taken to compose a [Text] composable from scratch with styled text as input.
      * This is the time taken to call the [Text] composable function.
      */
     @Test
     fun first_compose() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstCompose {
-                TextMultiStyleTestCase(
-                    width,
-                    fontSize,
-                    textLength,
-                    styleCount,
-                    textGenerator
-                )
-            }
-        }
+        benchmarkRule.benchmarkFirstCompose(caseFactory)
     }
 
     /**
@@ -86,17 +100,7 @@
      */
     @Test
     fun first_measure() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstMeasure {
-                TextMultiStyleTestCase(
-                    width,
-                    fontSize,
-                    textLength,
-                    styleCount,
-                    textGenerator
-                )
-            }
-        }
+        benchmarkRule.benchmarkFirstMeasure(caseFactory)
     }
 
     /**
@@ -105,17 +109,7 @@
      */
     @Test
     fun first_layout() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstLayout {
-                TextMultiStyleTestCase(
-                    width,
-                    fontSize,
-                    textLength,
-                    styleCount,
-                    textGenerator
-                )
-            }
-        }
+        benchmarkRule.benchmarkFirstLayout(caseFactory)
     }
 
     /**
@@ -124,17 +118,7 @@
      */
     @Test
     fun first_draw() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstDraw {
-                TextMultiStyleTestCase(
-                    width,
-                    fontSize,
-                    textLength,
-                    styleCount,
-                    textGenerator
-                )
-            }
-        }
+        benchmarkRule.benchmarkFirstDraw(caseFactory)
     }
 
     /**
@@ -144,17 +128,7 @@
      */
     @Test
     fun layout() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkLayoutPerf {
-                TextMultiStyleTestCase(
-                    width,
-                    fontSize,
-                    textLength,
-                    styleCount,
-                    textGenerator
-                )
-            }
-        }
+        benchmarkRule.benchmarkLayoutPerf(caseFactory)
     }
 
     /**
@@ -162,16 +136,6 @@
      */
     @Test
     fun draw() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkDrawPerf {
-                TextMultiStyleTestCase(
-                    width,
-                    fontSize,
-                    textLength,
-                    styleCount,
-                    textGenerator
-                )
-            }
-        }
+        benchmarkRule.benchmarkDrawPerf(caseFactory)
     }
 }
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextToggleTextBenchmark.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextToggleTextBenchmark.kt
index 184555c..6a1c24c 100644
--- a/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextToggleTextBenchmark.kt
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/TextToggleTextBenchmark.kt
@@ -51,9 +51,15 @@
     private val width = textBenchmarkRule.widthDp.dp
     private val fontSize = textBenchmarkRule.fontSizeSp.sp
 
-    private val toggleTextCaseFactory = {
+    private val caseFactory = {
         textBenchmarkRule.generator { generator ->
-            TextToggleTextTestCase(generator, textLength, width, fontSize)
+            TextToggleTextTestCase(
+                textGenerator = generator,
+                textLength = textLength,
+                textNumber = textBenchmarkRule.repeatTimes,
+                width = width,
+                fontSize = fontSize
+            )
         }
     }
 
@@ -62,7 +68,7 @@
      */
     @Test
     fun toggleText_recompose() {
-        benchmarkRule.toggleStateBenchmarkRecompose(toggleTextCaseFactory)
+        benchmarkRule.toggleStateBenchmarkRecompose(caseFactory)
     }
 
     /**
@@ -70,7 +76,7 @@
      */
     @Test
     fun toggleText_measure() {
-        benchmarkRule.toggleStateBenchmarkMeasure(toggleTextCaseFactory)
+        benchmarkRule.toggleStateBenchmarkMeasure(caseFactory)
     }
 
     /**
@@ -78,7 +84,7 @@
      */
     @Test
     fun toggleText_layout() {
-        benchmarkRule.toggleStateBenchmarkLayout(toggleTextCaseFactory)
+        benchmarkRule.toggleStateBenchmarkLayout(caseFactory)
     }
 
     /**
@@ -86,6 +92,6 @@
      */
     @Test
     fun toggleText_draw() {
-        benchmarkRule.toggleStateBenchmarkDraw(toggleTextCaseFactory)
+        benchmarkRule.toggleStateBenchmarkDraw(caseFactory)
     }
 }
\ No newline at end of file
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/view/AndroidTextViewBenchmark.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/view/AndroidTextViewBenchmark.kt
index f13a503..d82f070 100644
--- a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/view/AndroidTextViewBenchmark.kt
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/view/AndroidTextViewBenchmark.kt
@@ -50,40 +50,37 @@
     @get:Rule
     val benchmarkRule = AndroidBenchmarkRule()
 
+    private val caseFactory = {
+        textBenchmarkRule.generator { textGenerator ->
+            AndroidTextViewTestCase(
+                List(textBenchmarkRule.repeatTimes) {
+                    textGenerator.nextParagraph(textLength)
+                }
+            )
+        }
+    }
     @Test
     fun first_setContent() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstSetContent {
-                AndroidTextViewTestCase(textGenerator.nextParagraph(textLength))
-            }
-        }
+        benchmarkRule.benchmarkFirstSetContent(caseFactory)
     }
 
     @Test
     fun first_measure() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstMeasure {
-                AndroidTextViewTestCase(textGenerator.nextParagraph(textLength))
-            }
-        }
+        benchmarkRule.benchmarkFirstMeasure(caseFactory)
     }
 
     @Test
     fun first_setContentPlusMeasure() {
-        textBenchmarkRule.generator { textGenerator ->
-            with(benchmarkRule) {
-                runBenchmarkFor(
-                    { AndroidTextViewTestCase(textGenerator.nextParagraph(textLength)) }
-                ) {
-                    measureRepeated {
-                        setupContent()
-                        runWithTimingDisabled {
-                            requestLayout()
-                        }
-                        measure()
-                        runWithTimingDisabled {
-                            disposeContent()
-                        }
+        with(benchmarkRule) {
+            runBenchmarkFor(caseFactory) {
+                measureRepeated {
+                    setupContent()
+                    runWithTimingDisabled {
+                        requestLayout()
+                    }
+                    measure()
+                    runWithTimingDisabled {
+                        disposeContent()
                     }
                 }
             }
@@ -92,37 +89,21 @@
 
     @Test
     fun first_layout() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstLayout {
-                AndroidTextViewTestCase(textGenerator.nextParagraph(textLength))
-            }
-        }
+        benchmarkRule.benchmarkFirstLayout(caseFactory)
     }
 
     @Test
     fun first_draw() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkFirstDraw {
-                AndroidTextViewTestCase(textGenerator.nextParagraph(textLength))
-            }
-        }
+        benchmarkRule.benchmarkFirstDraw(caseFactory)
     }
 
     @Test
     fun layout() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkLayoutPerf {
-                AndroidTextViewTestCase(textGenerator.nextParagraph(textLength))
-            }
-        }
+        benchmarkRule.benchmarkLayoutPerf(caseFactory)
     }
 
     @Test
     fun draw() {
-        textBenchmarkRule.generator { textGenerator ->
-            benchmarkRule.benchmarkDrawPerf {
-                AndroidTextViewTestCase(textGenerator.nextParagraph(textLength))
-            }
-        }
+        benchmarkRule.benchmarkDrawPerf(caseFactory)
     }
 }
\ No newline at end of file
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/AndroidTapIntegrationBenchmark.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/AndroidTapIntegrationBenchmark.kt
new file mode 100644
index 0000000..4d84c53
--- /dev/null
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/AndroidTapIntegrationBenchmark.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.pointerinput
+
+import android.content.Context
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+/**
+ * Benchmark for simply tapping on an item in Android.
+ *
+ * The intent is to measure the speed of all parts necessary for a normal tap starting from
+ * MotionEvents getting dispatched to a particular view. The test therefore includes hit
+ * testing and dispatch.
+ *
+ * This is intended to be an equivalent counterpart to [ComposeTapIntegrationBenchmark].
+ *
+ * The hierarchy is set up to look like:
+ * rootView
+ *   -> LinearLayout
+ *     -> CustomView (with click listener)
+ *       -> TextView
+ *       -> TextView
+ *       -> TextView
+ *       -> ...
+ *
+ * MotionEvents are dispatched to rootView as ACTION_DOWN followed by ACTION_UP.  The validity of
+ * the test is verified in a custom click listener in CustomView with
+ * com.google.common.truth.Truth.assertThat and by counting the clicks in the click listener and
+ * later verifying that they count is sufficiently high.
+ *
+ * The reason a CustomView is used with a custom click listener is that View's normal click
+ * listener is called via a posted Runnable, which is problematic for the benchmark library and
+ * less equivalent to what Compose does anyway.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class AndroidTapIntegrationBenchmark {
+
+    private lateinit var rootView: View
+    private lateinit var expectedLabel: String
+
+    private var actualClickCount = 0
+    private var expectedClickCount = 0
+
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val activityTestRule = androidx.test.rule.ActivityTestRule(TestActivity::class.java)
+
+    @Before
+    fun setup() {
+        val activity = activityTestRule.activity
+        Assert.assertTrue(
+            "timed out waiting for activity focus",
+            activity.hasFocusLatch.await(5, TimeUnit.SECONDS)
+        )
+
+        rootView = activity.findViewById<ViewGroup>(android.R.id.content)
+
+        activityTestRule.runOnUiThread {
+
+            val children = (0 until NumItems).map { i ->
+                CustomView(activity).apply {
+                    layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, ItemHeightPx.toInt())
+                    label = "$i"
+                    clickListener = {
+                        assertThat(this.label).isEqualTo(expectedLabel)
+                        actualClickCount++
+                    }
+                }
+            }
+
+            val linearLayout = LinearLayout(activity).apply {
+                orientation = LinearLayout.VERTICAL
+                layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+                children.forEach {
+                    addView(it)
+                }
+            }
+
+            activity.setContentView(linearLayout)
+        }
+    }
+
+    // This test requires more hit test processing so changes to hit testing will be tracked more
+    // by this test.
+    @UiThreadTest
+    @Test
+    @Ignore("We don't want this to show up in our benchmark CI tests.")
+    fun clickOnLateItem() {
+        // As items that are laid out last are hit tested first (so z order is respected), item
+        // at 0 will be hit tested late.
+        clickOnItem(0, "0")
+    }
+
+    // This test requires less hit testing so changes to dispatch will be tracked more by this test.
+    @UiThreadTest
+    @Test
+    @Ignore("We don't want this to show up in our benchmark CI tests.")
+    fun clickOnEarlyItem() {
+        // As items that are laid out last are hit tested first (so z order is respected), item
+        // at NumItems - 1 will be hit tested early.
+        val lastItem = NumItems - 1
+        clickOnItem(lastItem, "$lastItem")
+    }
+
+    private fun clickOnItem(item: Int, expectedLabel: String) {
+
+        this.expectedLabel = expectedLabel
+
+        // half height of an item + top of the chosen item = middle of the chosen item
+        val y = (ItemHeightPx / 2) + (item * ItemHeightPx)
+
+        val down = MotionEvent(
+            0,
+            ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(0f, y)),
+            rootView
+        )
+
+        val up = MotionEvent(
+            10,
+            ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(0f, y)),
+            rootView
+        )
+
+        benchmarkRule.measureRepeated {
+            rootView.dispatchTouchEvent(down)
+            rootView.dispatchTouchEvent(up)
+            expectedClickCount++
+        }
+
+        assertThat(actualClickCount).isEqualTo(expectedClickCount)
+    }
+}
+
+private class CustomView(context: Context) : FrameLayout(context) {
+    var label: String
+        get() = textView.text.toString()
+        set(value) {
+            textView.text = value
+        }
+
+    lateinit var clickListener: () -> Unit
+
+    val textView: TextView = TextView(context).apply {
+        layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+    }
+
+    init {
+        addView(textView)
+    }
+
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        if (event!!.actionMasked == ACTION_UP) {
+            clickListener.invoke()
+        }
+
+        return true
+    }
+}
\ No newline at end of file
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/ComposeTapIntegrationBenchmark.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/ComposeTapIntegrationBenchmark.kt
new file mode 100644
index 0000000..8f592d5
--- /dev/null
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/ComposeTapIntegrationBenchmark.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.pointerinput
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.unit.dp
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+/**
+ * Benchmark for simply tapping on an item in Compose.
+ *
+ * The intent is to measure the speed of all parts necessary for a normal tap starting from
+ * [MotionEvent]s getting dispatched to a particular view.  The test therefore includes hit
+ * testing and dispatch.
+ *
+ * This is intended to be an equivalent counterpart to [AndroidTapIntegrationBenchmark].
+ *
+ * The hierarchy is set up to look like:
+ * rootView
+ *   -> Column
+ *     -> Text (with click listener)
+ *     -> Text (with click listener)
+ *     -> Text (with click listener)
+ *     -> ...
+ *
+ * MotionEvents are dispatched to rootView as ACTION_DOWN followed by ACTION_UP.  The validity of
+ * the test is verified inside the click listener with com.google.common.truth.Truth.assertThat
+ * and by counting the clicks in the click listener and later verifying that they count is
+ * sufficiently high.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ComposeTapIntegrationBenchmark {
+
+    private lateinit var rootView: View
+    private lateinit var expectedLabel: String
+
+    private var itemHeightDp = 0.dp // Is set to correct value during composition.
+    private var actualClickCount = 0
+    private var expectedClickCount = 0
+
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val activityTestRule = androidx.test.rule.ActivityTestRule(TestActivity::class.java)
+
+    @Before
+    fun setup() {
+        val activity = activityTestRule.activity
+        Assert.assertTrue(
+            "timed out waiting for activity focus",
+            activity.hasFocusLatch.await(5, TimeUnit.SECONDS)
+        )
+
+        rootView = activity.findViewById<ViewGroup>(android.R.id.content)
+
+        activityTestRule.runOnUiThreadIR {
+            activity.setContent {
+                with(DensityAmbient.current) {
+                    itemHeightDp = ItemHeightPx.toDp()
+                }
+                App()
+            }
+        }
+    }
+
+    // This test requires more hit test processing so changes to hit testing will be tracked more
+    // by this test.
+    @UiThreadTest
+    @Test
+    fun clickOnLateItem() {
+        // As items that are laid out last are hit tested first (so z order is respected), item
+        // at 0 will be hit tested late.
+        clickOnItem(0, "0")
+    }
+
+    // This test requires less hit testing so changes to dispatch will be tracked more by this test.
+    @UiThreadTest
+    @Test
+    fun clickOnEarlyItemFyi() {
+        // As items that are laid out last are hit tested first (so z order is respected), item
+        // at NumItems - 1 will be hit tested early.
+        val lastItem = NumItems - 1
+        clickOnItem(lastItem, "$lastItem")
+    }
+
+    private fun clickOnItem(item: Int, expectedLabel: String) {
+
+        this.expectedLabel = expectedLabel
+
+        // half height of an item + top of the chosen item = middle of the chosen item
+        val y = (ItemHeightPx / 2) + (item * ItemHeightPx)
+
+        val down = MotionEvent(
+            0,
+            android.view.MotionEvent.ACTION_DOWN,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(0f, y)),
+            rootView
+        )
+
+        val up = MotionEvent(
+            10,
+            android.view.MotionEvent.ACTION_UP,
+            1,
+            0,
+            arrayOf(PointerProperties(0)),
+            arrayOf(PointerCoords(0f, y)),
+            rootView
+        )
+
+        benchmarkRule.measureRepeated {
+            rootView.dispatchTouchEvent(down)
+            rootView.dispatchTouchEvent(up)
+            expectedClickCount++
+        }
+
+        assertThat(actualClickCount).isEqualTo(expectedClickCount)
+    }
+
+    @Composable
+    fun App() {
+        EmailList(NumItems)
+    }
+
+    @Composable
+    fun EmailList(count: Int) {
+        Column {
+            repeat(count) { i ->
+                Email("$i")
+            }
+        }
+    }
+
+    @Composable
+    fun Email(label: String) {
+        Text(
+            text = label,
+            modifier = Modifier
+                .clickable {
+                    assertThat(label).isEqualTo(expectedLabel)
+                    actualClickCount++
+                }
+                .fillMaxWidth()
+                .height(itemHeightDp)
+        )
+    }
+}
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TapIntegrationBenchmarkValues.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TapIntegrationBenchmarkValues.kt
new file mode 100644
index 0000000..903c918
--- /dev/null
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TapIntegrationBenchmarkValues.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.ui.pointerinput
+
+val ItemHeightPx = 1f
+val NumItems = 100
\ No newline at end of file
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TestActivity.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TestActivity.kt
new file mode 100644
index 0000000..811b1b8
--- /dev/null
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TestActivity.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.ui.pointerinput
+
+import androidx.activity.ComponentActivity
+import java.util.concurrent.CountDownLatch
+
+class TestActivity : ComponentActivity() {
+    var hasFocusLatch = CountDownLatch(1)
+
+    override fun onWindowFocusChanged(hasFocus: Boolean) {
+        super.onWindowFocusChanged(hasFocus)
+        if (hasFocus) {
+            hasFocusLatch.countDown()
+        }
+    }
+}
diff --git a/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/utils.kt b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/utils.kt
new file mode 100644
index 0000000..aeb62bf
--- /dev/null
+++ b/ui/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/utils.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.pointerinput
+
+import android.view.MotionEvent
+import android.view.View
+
+// We only need this because IR compiler doesn't like converting lambdas to Runnables
+@Suppress("DEPRECATION")
+internal fun androidx.test.rule.ActivityTestRule<*>.runOnUiThreadIR(block: () -> Unit) {
+    val runnable: Runnable = object : Runnable {
+        override fun run() {
+            block()
+        }
+    }
+    runOnUiThread(runnable)
+}
+
+/**
+ * Creates a simple [MotionEvent].
+ *
+ * @param dispatchTarget The [View] that the [MotionEvent] is going to be dispatched to. This
+ * guarantees that the MotionEvent is created correctly for both Compose (which relies on raw
+ * coordinates being correct) and Android (which requires that local coordinates are correct).
+ */
+internal fun MotionEvent(
+    eventTime: Int,
+    action: Int,
+    numPointers: Int,
+    actionIndex: Int,
+    pointerProperties: Array<MotionEvent.PointerProperties>,
+    pointerCoords: Array<MotionEvent.PointerCoords>,
+    dispatchTarget: View
+): MotionEvent {
+
+    val locationOnScreen = IntArray(2) { 0 }
+    dispatchTarget.getLocationOnScreen(locationOnScreen)
+
+    pointerCoords.forEach {
+        it.x += locationOnScreen[0]
+        it.y += locationOnScreen[1]
+    }
+
+    val motionEvent = MotionEvent.obtain(
+        0,
+        eventTime.toLong(),
+        action + (actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+        numPointers,
+        pointerProperties,
+        pointerCoords,
+        0,
+        0,
+        0f,
+        0f,
+        0,
+        0,
+        0,
+        0
+    ).apply {
+        offsetLocation(-locationOnScreen[0].toFloat(), -locationOnScreen[1].toFloat())
+    }
+
+    pointerCoords.forEach {
+        it.x -= locationOnScreen[0]
+        it.y -= locationOnScreen[1]
+    }
+
+    return motionEvent
+}
+
+@Suppress("RemoveRedundantQualifierName")
+internal fun PointerProperties(id: Int) =
+    MotionEvent.PointerProperties().apply { this.id = id }
+
+@Suppress("RemoveRedundantQualifierName")
+internal fun PointerCoords(x: Float, y: Float) =
+    MotionEvent.PointerCoords().apply {
+        this.x = x
+        this.y = y
+    }
\ No newline at end of file
diff --git a/ui/integration-tests/demos/src/androidTest/java/androidx/ui/demos/test/DemoTest.kt b/ui/integration-tests/demos/src/androidTest/java/androidx/ui/demos/test/DemoTest.kt
index 73dc96b..08338bc 100644
--- a/ui/integration-tests/demos/src/androidTest/java/androidx/ui/demos/test/DemoTest.kt
+++ b/ui/integration-tests/demos/src/androidTest/java/androidx/ui/demos/test/DemoTest.kt
@@ -100,7 +100,6 @@
 
     @LargeTest
     @Test
-    @Ignore("b/162824105")
     fun navigateThroughAllDemos() {
         // Keep track of each demo we visit
         val visitedDemos = mutableListOf<Demo>()
diff --git a/ui/integration-tests/src/main/java/androidx/ui/integration/test/TextBenchmarkTestRule.kt b/ui/integration-tests/src/main/java/androidx/ui/integration/test/TextBenchmarkTestRule.kt
index afd8434..a81e39d 100644
--- a/ui/integration-tests/src/main/java/androidx/ui/integration/test/TextBenchmarkTestRule.kt
+++ b/ui/integration-tests/src/main/java/androidx/ui/integration/test/TextBenchmarkTestRule.kt
@@ -54,6 +54,10 @@
     // fontSize here are dp and sp, which should be converted into needed unit in the test case.
     val widthDp: Float = 160f
     val fontSizeSp: Float = 8f
+
+    // We noticed that benchmark a single composable Text will lead to inaccurate result. To fix
+    // this problem, we benchmark a column of Texts with its length equal to [repeatTimes].
+    val repeatTimes: Int = 10
 }
 
 /**
diff --git a/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextBasicTestCase.kt b/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextInColumnTestCase.kt
similarity index 86%
rename from ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextBasicTestCase.kt
rename to ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextInColumnTestCase.kt
index 102cc50..a4f4579 100644
--- a/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextBasicTestCase.kt
+++ b/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextInColumnTestCase.kt
@@ -20,12 +20,13 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.foundation.Box
 import androidx.compose.foundation.Text
+import androidx.compose.foundation.layout.Column
 import androidx.compose.ui.graphics.Color
 import androidx.ui.test.ToggleableTestCase
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
 import androidx.ui.test.LayeredComposeTestCase
@@ -33,8 +34,8 @@
 /**
  * The benchmark test case for [Text], where the input is a plain string.
  */
-class TextBasicTestCase(
-    private val text: String,
+class TextInColumnTestCase(
+    private val texts: List<AnnotatedString>,
     private val width: Dp,
     private val fontSize: TextUnit
 ) : LayeredComposeTestCase, ToggleableTestCase {
@@ -43,17 +44,20 @@
 
     @Composable
     override fun emitMeasuredContent() {
-        Text(text = text, color = color.value, fontSize = fontSize)
+        for (text in texts) {
+            Text(text = text, color = color.value, fontSize = fontSize)
+        }
     }
 
     @Composable
     override fun emitContentWrappers(content: @Composable () -> Unit) {
-        Box(
+        Column(
             modifier = Modifier.wrapContentSize(Alignment.Center).preferredWidth(width)
         ) {
             content()
         }
     }
+
     override fun toggleState() {
         if (color.value == Color.Black) {
             color.value = Color.Red
diff --git a/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextMultiStyleTestCase.kt b/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextMultiStyleTestCase.kt
deleted file mode 100644
index 7e89dbf..0000000
--- a/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextMultiStyleTestCase.kt
+++ /dev/null
@@ -1,62 +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.ui.integration.test.core.text
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.foundation.Text
-import androidx.compose.ui.graphics.Color
-import androidx.ui.integration.test.RandomTextGenerator
-import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.ui.test.ComposeTestCase
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.TextUnit
-
-/**
- * The benchmark test case for [Text], where the input is an [AnnotatedString] with [TextStyle]s
- * on it.
- */
-class TextMultiStyleTestCase(
-    private val width: Dp,
-    private val fontSize: TextUnit,
-    textLength: Int,
-    styleCount: Int,
-    randomTextGenerator: RandomTextGenerator
-) : ComposeTestCase {
-
-    /**
-     * Trick to avoid the text word cache.
-     * @see TextBasicTestCase.text
-     */
-    private val text = randomTextGenerator.nextAnnotatedString(
-        length = textLength,
-        styleCount = styleCount,
-        hasMetricAffectingStyle = true
-    )
-
-    @Composable
-    override fun emitContent() {
-        Text(
-            text = text, color = Color.Black, fontSize = fontSize,
-            modifier = Modifier.wrapContentSize(Alignment.Center).preferredWidth(width)
-        )
-    }
-}
\ No newline at end of file
diff --git a/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextToggleTextTestCase.kt b/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextToggleTextTestCase.kt
index 69fdf8a..891ac21 100644
--- a/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextToggleTextTestCase.kt
+++ b/ui/integration-tests/src/main/java/androidx/ui/integration/test/core/text/TextToggleTextTestCase.kt
@@ -17,8 +17,8 @@
 package androidx.ui.integration.test.core.text
 
 import androidx.compose.runtime.Composable
-import androidx.compose.foundation.Box
 import androidx.compose.foundation.Text
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.runtime.mutableStateOf
@@ -34,22 +34,31 @@
 class TextToggleTextTestCase(
     private val textGenerator: RandomTextGenerator,
     private val textLength: Int,
+    private val textNumber: Int,
     private val width: Dp,
     private val fontSize: TextUnit
 ) : ComposeTestCase, ToggleableTestCase {
 
-    val text = mutableStateOf(textGenerator.nextParagraph(length = textLength))
+    private val texts = mutableStateOf(
+        List(textNumber) {
+            textGenerator.nextParagraph(length = textLength)
+        }
+    )
 
     @Composable
     override fun emitContent() {
-        Box(
+        Column(
             modifier = Modifier.wrapContentSize(Alignment.Center).preferredWidth(width)
         ) {
-            Text(text = text.value, color = Color.Black, fontSize = fontSize)
+            for (text in texts.value) {
+                Text(text = text, color = Color.Black, fontSize = fontSize)
+            }
         }
     }
 
     override fun toggleState() {
-        text.value = textGenerator.nextParagraph(length = textLength)
+        texts.value = List(textNumber) {
+            textGenerator.nextParagraph(length = textLength)
+        }
     }
 }
\ No newline at end of file
diff --git a/ui/integration-tests/src/main/java/androidx/ui/integration/test/view/AndroidTextViewTestCase.kt b/ui/integration-tests/src/main/java/androidx/ui/integration/test/view/AndroidTextViewTestCase.kt
index 2dfbcc5..0db1c47 100644
--- a/ui/integration-tests/src/main/java/androidx/ui/integration/test/view/AndroidTextViewTestCase.kt
+++ b/ui/integration-tests/src/main/java/androidx/ui/integration/test/view/AndroidTextViewTestCase.kt
@@ -19,7 +19,7 @@
 import android.app.Activity
 import android.util.TypedValue
 import android.view.ViewGroup
-import android.widget.FrameLayout
+import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.ui.benchmark.android.AndroidTestCase
 import kotlin.math.roundToInt
@@ -28,28 +28,35 @@
  * Version of [androidx.ui.integration.test.core.text.TextBasicTestCase] using Android views.
  */
 class AndroidTextViewTestCase(
-    val text: String
+    private val texts: List<String>
 ) : AndroidTestCase {
 
     private var fontSize = 8f
 
     override fun getContent(activity: Activity): ViewGroup {
-        val frameLayout = FrameLayout(activity)
-        val textView = TextView(activity)
-        textView.text = text
-        textView.layoutParams = ViewGroup.LayoutParams(
-            ViewGroup.LayoutParams.WRAP_CONTENT,
+        val column = LinearLayout(activity)
+        column.orientation = LinearLayout.VERTICAL
+        column.layoutParams = ViewGroup.LayoutParams(
+            ViewGroup.LayoutParams.MATCH_PARENT,
             ViewGroup.LayoutParams.WRAP_CONTENT
         )
+        for (text in texts) {
+            val textView = TextView(activity)
+            textView.text = text
+            textView.layoutParams = ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
 
-        textView.width = TypedValue.applyDimension(
-            TypedValue.COMPLEX_UNIT_DIP,
-            160f,
-            activity.resources.displayMetrics
-        ).roundToInt()
+            textView.width = TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                160f,
+                activity.resources.displayMetrics
+            ).roundToInt()
 
-        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
-        frameLayout.addView(textView)
-        return frameLayout
+            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
+            column.addView(textView)
+        }
+        return column
     }
 }
diff --git a/ui/ui-foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt b/ui/ui-foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
index d10d774..b073db6 100644
--- a/ui/ui-foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
+++ b/ui/ui-foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
@@ -46,7 +46,6 @@
 import org.junit.Test
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
-import kotlin.math.roundToInt
 
 @LargeTest
 @OptIn(
@@ -142,19 +141,19 @@
     }
 
     private fun Bitmap.assertCursor(cursorWidth: Dp, density: Density) {
-        val halfCursorWidth = (with(density) { cursorWidth.toIntPx() } / 2f).roundToInt()
+        val сursorWidth = (with(density) { cursorWidth.toIntPx() })
         val width = width
         val height = height
         this.assertPixels(
             IntSize(width, height)
         ) { position ->
-            if (position.x >= halfCursorWidth - 1 && position.x < halfCursorWidth + 1) {
+            if (position.x >= сursorWidth - 1 && position.x < сursorWidth + 1) {
                 // skip some pixels around cursor
                 null
             } else if (position.y < 5 || position.y > height - 5) {
                 // skip some pixels vertically
                 null
-            } else if (position.x in 0..halfCursorWidth) {
+            } else if (position.x in 0..сursorWidth) {
                 // cursor
                 Color.Red
             } else {
diff --git a/ui/ui-foundation/src/commonMain/kotlin/androidx/compose/foundation/BaseTextField.kt b/ui/ui-foundation/src/commonMain/kotlin/androidx/compose/foundation/BaseTextField.kt
index b78fc90..0a15d0c 100644
--- a/ui/ui-foundation/src/commonMain/kotlin/androidx/compose/foundation/BaseTextField.kt
+++ b/ui/ui-foundation/src/commonMain/kotlin/androidx/compose/foundation/BaseTextField.kt
@@ -237,7 +237,8 @@
                 0f, 0f,
                 cursorWidth, cursorHeight
             )
-            val cursorX = (cursorRect.left + cursorRect.right) / 2
+            val cursorX = (cursorRect.left + cursorWidth / 2)
+                .coerceAtMost(size.width - cursorWidth / 2)
 
             drawLine(
                 color.value,
diff --git a/ui/ui-material/icons/generator/api/icons.txt b/ui/ui-material/icons/generator/api/icons.txt
index 68df3a8..f03cb43 100644
--- a/ui/ui-material/icons/generator/api/icons.txt
+++ b/ui/ui-material/icons/generator/api/icons.txt
@@ -28,6 +28,7 @@
 Filled.AddPhotoAlternate
 Filled.AddRoad
 Filled.AddShoppingCart
+Filled.AddTask
 Filled.AddToHomeScreen
 Filled.AddToPhotos
 Filled.AddToQueue
@@ -255,6 +256,7 @@
 Filled.ConnectWithoutContact
 Filled.Construction
 Filled.ContactMail
+Filled.ContactPage
 Filled.ContactPhone
 Filled.ContactSupport
 Filled.Contactless
@@ -319,6 +321,7 @@
 Filled.DirectionsSubway
 Filled.DirectionsTransit
 Filled.DirectionsWalk
+Filled.DisabledByDefault
 Filled.DiscFull
 Filled.Dns
 Filled.DoNotStep
@@ -395,6 +398,7 @@
 Filled.ExposureZero
 Filled.Extension
 Filled.Face
+Filled.Facebook
 Filled.FactCheck
 Filled.FamilyRestroom
 Filled.FastForward
@@ -516,6 +520,7 @@
 Filled.Group
 Filled.GroupAdd
 Filled.GroupWork
+Filled.Groups
 Filled.Handyman
 Filled.Hd
 Filled.HdrOff
@@ -677,6 +682,7 @@
 Filled.Loupe
 Filled.LowPriority
 Filled.Loyalty
+Filled.Luggage
 Filled.Mail
 Filled.MailOutline
 Filled.Map
@@ -724,6 +730,7 @@
 Filled.MoreTime
 Filled.MoreVert
 Filled.MotionPhotosOn
+Filled.MotionPhotosPause
 Filled.MotionPhotosPaused
 Filled.Motorcycle
 Filled.Mouse
@@ -754,11 +761,13 @@
 Filled.Nfc
 Filled.NightShelter
 Filled.NightsStay
+Filled.NoBackpack
 Filled.NoCell
 Filled.NoDrinks
 Filled.NoEncryption
 Filled.NoFlash
 Filled.NoFood
+Filled.NoLuggage
 Filled.NoMeals
 Filled.NoMeetingRoom
 Filled.NoPhotography
@@ -790,6 +799,7 @@
 Filled.OpenInFull
 Filled.OpenInNew
 Filled.OpenWith
+Filled.Outbond
 Filled.OutdoorGrill
 Filled.Outlet
 Filled.OutlinedFlag
@@ -902,6 +912,7 @@
 Filled.Public
 Filled.PublicOff
 Filled.Publish
+Filled.PublishedWithChanges
 Filled.PushPin
 Filled.QrCode
 Filled.QrCodeScanner
@@ -942,6 +953,7 @@
 Filled.Report
 Filled.ReportOff
 Filled.ReportProblem
+Filled.RequestPage
 Filled.RequestQuote
 Filled.Restaurant
 Filled.RestaurantMenu
@@ -1082,6 +1094,7 @@
 Filled.SportsTennis
 Filled.SportsVolleyball
 Filled.SquareFoot
+Filled.StackedLineChart
 Filled.Stairs
 Filled.Star
 Filled.StarBorder
@@ -1206,6 +1219,7 @@
 Filled.Undo
 Filled.UnfoldLess
 Filled.UnfoldMore
+Filled.Unpublished
 Filled.Unsubscribe
 Filled.Update
 Filled.Upgrade
@@ -1323,6 +1337,7 @@
 Outlined.AddPhotoAlternate
 Outlined.AddRoad
 Outlined.AddShoppingCart
+Outlined.AddTask
 Outlined.AddToHomeScreen
 Outlined.AddToPhotos
 Outlined.AddToQueue
@@ -1550,6 +1565,7 @@
 Outlined.ConnectWithoutContact
 Outlined.Construction
 Outlined.ContactMail
+Outlined.ContactPage
 Outlined.ContactPhone
 Outlined.ContactSupport
 Outlined.Contactless
@@ -1614,6 +1630,7 @@
 Outlined.DirectionsSubway
 Outlined.DirectionsTransit
 Outlined.DirectionsWalk
+Outlined.DisabledByDefault
 Outlined.DiscFull
 Outlined.Dns
 Outlined.DoNotStep
@@ -1690,6 +1707,7 @@
 Outlined.ExposureZero
 Outlined.Extension
 Outlined.Face
+Outlined.Facebook
 Outlined.FactCheck
 Outlined.FamilyRestroom
 Outlined.FastForward
@@ -1811,6 +1829,7 @@
 Outlined.Group
 Outlined.GroupAdd
 Outlined.GroupWork
+Outlined.Groups
 Outlined.Handyman
 Outlined.Hd
 Outlined.HdrOff
@@ -1972,6 +1991,7 @@
 Outlined.Loupe
 Outlined.LowPriority
 Outlined.Loyalty
+Outlined.Luggage
 Outlined.Mail
 Outlined.MailOutline
 Outlined.Map
@@ -2019,6 +2039,7 @@
 Outlined.MoreTime
 Outlined.MoreVert
 Outlined.MotionPhotosOn
+Outlined.MotionPhotosPause
 Outlined.MotionPhotosPaused
 Outlined.Motorcycle
 Outlined.Mouse
@@ -2049,11 +2070,13 @@
 Outlined.Nfc
 Outlined.NightShelter
 Outlined.NightsStay
+Outlined.NoBackpack
 Outlined.NoCell
 Outlined.NoDrinks
 Outlined.NoEncryption
 Outlined.NoFlash
 Outlined.NoFood
+Outlined.NoLuggage
 Outlined.NoMeals
 Outlined.NoMeetingRoom
 Outlined.NoPhotography
@@ -2085,6 +2108,7 @@
 Outlined.OpenInFull
 Outlined.OpenInNew
 Outlined.OpenWith
+Outlined.Outbond
 Outlined.OutdoorGrill
 Outlined.Outlet
 Outlined.OutlinedFlag
@@ -2197,6 +2221,7 @@
 Outlined.Public
 Outlined.PublicOff
 Outlined.Publish
+Outlined.PublishedWithChanges
 Outlined.PushPin
 Outlined.QrCode
 Outlined.QrCodeScanner
@@ -2237,6 +2262,7 @@
 Outlined.Report
 Outlined.ReportOff
 Outlined.ReportProblem
+Outlined.RequestPage
 Outlined.RequestQuote
 Outlined.Restaurant
 Outlined.RestaurantMenu
@@ -2377,6 +2403,7 @@
 Outlined.SportsTennis
 Outlined.SportsVolleyball
 Outlined.SquareFoot
+Outlined.StackedLineChart
 Outlined.Stairs
 Outlined.Star
 Outlined.StarBorder
@@ -2501,6 +2528,7 @@
 Outlined.Undo
 Outlined.UnfoldLess
 Outlined.UnfoldMore
+Outlined.Unpublished
 Outlined.Unsubscribe
 Outlined.Update
 Outlined.Upgrade
@@ -2618,6 +2646,7 @@
 Rounded.AddPhotoAlternate
 Rounded.AddRoad
 Rounded.AddShoppingCart
+Rounded.AddTask
 Rounded.AddToHomeScreen
 Rounded.AddToPhotos
 Rounded.AddToQueue
@@ -2845,6 +2874,7 @@
 Rounded.ConnectWithoutContact
 Rounded.Construction
 Rounded.ContactMail
+Rounded.ContactPage
 Rounded.ContactPhone
 Rounded.ContactSupport
 Rounded.Contactless
@@ -2909,6 +2939,7 @@
 Rounded.DirectionsSubway
 Rounded.DirectionsTransit
 Rounded.DirectionsWalk
+Rounded.DisabledByDefault
 Rounded.DiscFull
 Rounded.Dns
 Rounded.DoNotStep
@@ -2985,6 +3016,7 @@
 Rounded.ExposureZero
 Rounded.Extension
 Rounded.Face
+Rounded.Facebook
 Rounded.FactCheck
 Rounded.FamilyRestroom
 Rounded.FastForward
@@ -3106,6 +3138,7 @@
 Rounded.Group
 Rounded.GroupAdd
 Rounded.GroupWork
+Rounded.Groups
 Rounded.Handyman
 Rounded.Hd
 Rounded.HdrOff
@@ -3267,6 +3300,7 @@
 Rounded.Loupe
 Rounded.LowPriority
 Rounded.Loyalty
+Rounded.Luggage
 Rounded.Mail
 Rounded.MailOutline
 Rounded.Map
@@ -3314,6 +3348,7 @@
 Rounded.MoreTime
 Rounded.MoreVert
 Rounded.MotionPhotosOn
+Rounded.MotionPhotosPause
 Rounded.MotionPhotosPaused
 Rounded.Motorcycle
 Rounded.Mouse
@@ -3344,11 +3379,13 @@
 Rounded.Nfc
 Rounded.NightShelter
 Rounded.NightsStay
+Rounded.NoBackpack
 Rounded.NoCell
 Rounded.NoDrinks
 Rounded.NoEncryption
 Rounded.NoFlash
 Rounded.NoFood
+Rounded.NoLuggage
 Rounded.NoMeals
 Rounded.NoMeetingRoom
 Rounded.NoPhotography
@@ -3380,6 +3417,7 @@
 Rounded.OpenInFull
 Rounded.OpenInNew
 Rounded.OpenWith
+Rounded.Outbond
 Rounded.OutdoorGrill
 Rounded.Outlet
 Rounded.OutlinedFlag
@@ -3492,6 +3530,7 @@
 Rounded.Public
 Rounded.PublicOff
 Rounded.Publish
+Rounded.PublishedWithChanges
 Rounded.PushPin
 Rounded.QrCode
 Rounded.QrCodeScanner
@@ -3532,6 +3571,7 @@
 Rounded.Report
 Rounded.ReportOff
 Rounded.ReportProblem
+Rounded.RequestPage
 Rounded.RequestQuote
 Rounded.Restaurant
 Rounded.RestaurantMenu
@@ -3672,6 +3712,7 @@
 Rounded.SportsTennis
 Rounded.SportsVolleyball
 Rounded.SquareFoot
+Rounded.StackedLineChart
 Rounded.Stairs
 Rounded.Star
 Rounded.StarBorder
@@ -3796,6 +3837,7 @@
 Rounded.Undo
 Rounded.UnfoldLess
 Rounded.UnfoldMore
+Rounded.Unpublished
 Rounded.Unsubscribe
 Rounded.Update
 Rounded.Upgrade
@@ -3913,6 +3955,7 @@
 Sharp.AddPhotoAlternate
 Sharp.AddRoad
 Sharp.AddShoppingCart
+Sharp.AddTask
 Sharp.AddToHomeScreen
 Sharp.AddToPhotos
 Sharp.AddToQueue
@@ -4140,6 +4183,7 @@
 Sharp.ConnectWithoutContact
 Sharp.Construction
 Sharp.ContactMail
+Sharp.ContactPage
 Sharp.ContactPhone
 Sharp.ContactSupport
 Sharp.Contactless
@@ -4204,6 +4248,7 @@
 Sharp.DirectionsSubway
 Sharp.DirectionsTransit
 Sharp.DirectionsWalk
+Sharp.DisabledByDefault
 Sharp.DiscFull
 Sharp.Dns
 Sharp.DoNotStep
@@ -4280,6 +4325,7 @@
 Sharp.ExposureZero
 Sharp.Extension
 Sharp.Face
+Sharp.Facebook
 Sharp.FactCheck
 Sharp.FamilyRestroom
 Sharp.FastForward
@@ -4401,6 +4447,7 @@
 Sharp.Group
 Sharp.GroupAdd
 Sharp.GroupWork
+Sharp.Groups
 Sharp.Handyman
 Sharp.Hd
 Sharp.HdrOff
@@ -4562,6 +4609,7 @@
 Sharp.Loupe
 Sharp.LowPriority
 Sharp.Loyalty
+Sharp.Luggage
 Sharp.Mail
 Sharp.MailOutline
 Sharp.Map
@@ -4609,6 +4657,7 @@
 Sharp.MoreTime
 Sharp.MoreVert
 Sharp.MotionPhotosOn
+Sharp.MotionPhotosPause
 Sharp.MotionPhotosPaused
 Sharp.Motorcycle
 Sharp.Mouse
@@ -4639,11 +4688,13 @@
 Sharp.Nfc
 Sharp.NightShelter
 Sharp.NightsStay
+Sharp.NoBackpack
 Sharp.NoCell
 Sharp.NoDrinks
 Sharp.NoEncryption
 Sharp.NoFlash
 Sharp.NoFood
+Sharp.NoLuggage
 Sharp.NoMeals
 Sharp.NoMeetingRoom
 Sharp.NoPhotography
@@ -4675,6 +4726,7 @@
 Sharp.OpenInFull
 Sharp.OpenInNew
 Sharp.OpenWith
+Sharp.Outbond
 Sharp.OutdoorGrill
 Sharp.Outlet
 Sharp.OutlinedFlag
@@ -4787,6 +4839,7 @@
 Sharp.Public
 Sharp.PublicOff
 Sharp.Publish
+Sharp.PublishedWithChanges
 Sharp.PushPin
 Sharp.QrCode
 Sharp.QrCodeScanner
@@ -4827,6 +4880,7 @@
 Sharp.Report
 Sharp.ReportOff
 Sharp.ReportProblem
+Sharp.RequestPage
 Sharp.RequestQuote
 Sharp.Restaurant
 Sharp.RestaurantMenu
@@ -4967,6 +5021,7 @@
 Sharp.SportsTennis
 Sharp.SportsVolleyball
 Sharp.SquareFoot
+Sharp.StackedLineChart
 Sharp.Stairs
 Sharp.Star
 Sharp.StarBorder
@@ -5091,6 +5146,7 @@
 Sharp.Undo
 Sharp.UnfoldLess
 Sharp.UnfoldMore
+Sharp.Unpublished
 Sharp.Unsubscribe
 Sharp.Update
 Sharp.Upgrade
@@ -5208,6 +5264,7 @@
 TwoTone.AddPhotoAlternate
 TwoTone.AddRoad
 TwoTone.AddShoppingCart
+TwoTone.AddTask
 TwoTone.AddToHomeScreen
 TwoTone.AddToPhotos
 TwoTone.AddToQueue
@@ -5435,6 +5492,7 @@
 TwoTone.ConnectWithoutContact
 TwoTone.Construction
 TwoTone.ContactMail
+TwoTone.ContactPage
 TwoTone.ContactPhone
 TwoTone.ContactSupport
 TwoTone.Contactless
@@ -5499,6 +5557,7 @@
 TwoTone.DirectionsSubway
 TwoTone.DirectionsTransit
 TwoTone.DirectionsWalk
+TwoTone.DisabledByDefault
 TwoTone.DiscFull
 TwoTone.Dns
 TwoTone.DoNotStep
@@ -5575,6 +5634,7 @@
 TwoTone.ExposureZero
 TwoTone.Extension
 TwoTone.Face
+TwoTone.Facebook
 TwoTone.FactCheck
 TwoTone.FamilyRestroom
 TwoTone.FastForward
@@ -5696,6 +5756,7 @@
 TwoTone.Group
 TwoTone.GroupAdd
 TwoTone.GroupWork
+TwoTone.Groups
 TwoTone.Handyman
 TwoTone.Hd
 TwoTone.HdrOff
@@ -5857,6 +5918,7 @@
 TwoTone.Loupe
 TwoTone.LowPriority
 TwoTone.Loyalty
+TwoTone.Luggage
 TwoTone.Mail
 TwoTone.MailOutline
 TwoTone.Map
@@ -5904,6 +5966,7 @@
 TwoTone.MoreTime
 TwoTone.MoreVert
 TwoTone.MotionPhotosOn
+TwoTone.MotionPhotosPause
 TwoTone.MotionPhotosPaused
 TwoTone.Motorcycle
 TwoTone.Mouse
@@ -5934,11 +5997,13 @@
 TwoTone.Nfc
 TwoTone.NightShelter
 TwoTone.NightsStay
+TwoTone.NoBackpack
 TwoTone.NoCell
 TwoTone.NoDrinks
 TwoTone.NoEncryption
 TwoTone.NoFlash
 TwoTone.NoFood
+TwoTone.NoLuggage
 TwoTone.NoMeals
 TwoTone.NoMeetingRoom
 TwoTone.NoPhotography
@@ -5970,6 +6035,7 @@
 TwoTone.OpenInFull
 TwoTone.OpenInNew
 TwoTone.OpenWith
+TwoTone.Outbond
 TwoTone.OutdoorGrill
 TwoTone.Outlet
 TwoTone.OutlinedFlag
@@ -6082,6 +6148,7 @@
 TwoTone.Public
 TwoTone.PublicOff
 TwoTone.Publish
+TwoTone.PublishedWithChanges
 TwoTone.PushPin
 TwoTone.QrCode
 TwoTone.QrCodeScanner
@@ -6122,6 +6189,7 @@
 TwoTone.Report
 TwoTone.ReportOff
 TwoTone.ReportProblem
+TwoTone.RequestPage
 TwoTone.RequestQuote
 TwoTone.Restaurant
 TwoTone.RestaurantMenu
@@ -6262,6 +6330,7 @@
 TwoTone.SportsTennis
 TwoTone.SportsVolleyball
 TwoTone.SquareFoot
+TwoTone.StackedLineChart
 TwoTone.Stairs
 TwoTone.Star
 TwoTone.StarBorder
@@ -6386,6 +6455,7 @@
 TwoTone.Undo
 TwoTone.UnfoldLess
 TwoTone.UnfoldMore
+TwoTone.Unpublished
 TwoTone.Unsubscribe
 TwoTone.Update
 TwoTone.Upgrade
diff --git a/ui/ui-material/icons/generator/build.gradle b/ui/ui-material/icons/generator/build.gradle
index 9158245..20f02c2 100644
--- a/ui/ui-material/icons/generator/build.gradle
+++ b/ui/ui-material/icons/generator/build.gradle
@@ -17,6 +17,7 @@
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 import static androidx.build.dependencies.DependenciesKt.*
 
@@ -50,3 +51,9 @@
     description = "Generator module that parses XML drawables to generate programmatic " +
             "representations of Material Icons."
 }
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
+    }
+}
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/add_task.xml b/ui/ui-material/icons/generator/raw-icons/filled/add_task.xml
new file mode 100644
index 0000000..295ddfa
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/add_task.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,5.18L10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l10,-10L22,5.18zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8c1.57,0 3.04,0.46 4.28,1.25l1.45,-1.45C16.1,2.67 14.13,2 12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.73,0 3.36,-0.44 4.78,-1.22l-1.5,-1.5C14.28,19.74 13.17,20 12,20zM19,15h-3v2h3v3h2v-3h3v-2h-3v-3h-2V15z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/book_online.xml b/ui/ui-material/icons/generator/raw-icons/filled/book_online.xml
index d240782..75849a8 100644
--- a/ui/ui-material/icons/generator/raw-icons/filled/book_online.xml
+++ b/ui/ui-material/icons/generator/raw-icons/filled/book_online.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M13.35,20l0.57,2H5c-1.11,0 -2,-0.9 -2,-2L3.01,6c0,-1.1 0.88,-2 1.99,-2h1V2h2v2h8V2h2v2h1c1.1,0 2,0.9 2,2v8.92l-2,-0.57V10H5v10H13.35zM21.71,21.29l-3.22,-3.22L21,17l-7,-2l2,7l1.08,-2.51l3.22,3.22L21.71,21.29zM12,17v-5H7v5H12z"/>
+      android:pathData="M17,1H7C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1 17,1zM7,18V6h10v12H7zM16,11V9.14C16,8.51 15.55,8 15,8H9C8.45,8 8,8.51 8,9.14l0,1.96c0.55,0 1,0.45 1,1c0,0.55 -0.45,1 -1,1l0,1.76C8,15.49 8.45,16 9,16h6c0.55,0 1,-0.51 1,-1.14V13c-0.55,0 -1,-0.45 -1,-1C15,11.45 15.45,11 16,11zM12.5,14.5h-1v-1h1V14.5zM12.5,12.5h-1v-1h1V12.5zM12.5,10.5h-1v-1h1V10.5z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/contact_page.xml b/ui/ui-material/icons/generator/raw-icons/filled/contact_page.xml
new file mode 100644
index 0000000..3905bf0
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/contact_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM12,10c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2C10,10.9 10.9,10 12,10zM16,18H8v-0.57c0,-0.81 0.48,-1.53 1.22,-1.85C10.07,15.21 11.01,15 12,15c0.99,0 1.93,0.21 2.78,0.58C15.52,15.9 16,16.62 16,17.43V18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/details.xml b/ui/ui-material/icons/generator/raw-icons/filled/details.xml
index 239c41e..91fe861 100644
--- a/ui/ui-material/icons/generator/raw-icons/filled/details.xml
+++ b/ui/ui-material/icons/generator/raw-icons/filled/details.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M3,4l9,16 9,-16L3,4zM6.38,6h11.25L12,16 6.38,6z"/>
+      android:pathData="M12,3L2,21h20L12,3zM13,8.92L18.6,19H13V8.92zM11,8.92V19H5.4L11,8.92z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/disabled_by_default.xml b/ui/ui-material/icons/generator/raw-icons/filled/disabled_by_default.xml
new file mode 100644
index 0000000..14e55ee
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/disabled_by_default.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M3,3v18h18V3H3zM17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/facebook.xml b/ui/ui-material/icons/generator/raw-icons/filled/facebook.xml
new file mode 100644
index 0000000..308115a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/facebook.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,4.84 3.44,8.87 8,9.8V15H8v-3h2V9.5C10,7.57 11.57,6 13.5,6H16v3h-2c-0.55,0 -1,0.45 -1,1v2h3v3h-3v6.95C18.05,21.45 22,17.19 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/groups.xml b/ui/ui-material/icons/generator/raw-icons/filled/groups.xml
new file mode 100644
index 0000000..a6df105
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/groups.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,12.75c1.63,0 3.07,0.39 4.24,0.9c1.08,0.48 1.76,1.56 1.76,2.73L18,18H6l0,-1.61c0,-1.18 0.68,-2.26 1.76,-2.73C8.93,13.14 10.37,12.75 12,12.75zM4,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C2,12.1 2.9,13 4,13zM5.13,14.1C4.76,14.04 4.39,14 4,14c-0.99,0 -1.93,0.21 -2.78,0.58C0.48,14.9 0,15.62 0,16.43V18l4.5,0v-1.61C4.5,15.56 4.73,14.78 5.13,14.1zM20,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C18,12.1 18.9,13 20,13zM24,16.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C21.93,14.21 20.99,14 20,14c-0.39,0 -0.76,0.04 -1.13,0.1c0.4,0.68 0.63,1.46 0.63,2.29V18l4.5,0V16.43zM12,6c1.66,0 3,1.34 3,3c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3C9,7.34 10.34,6 12,6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/luggage.xml b/ui/ui-material/icons/generator/raw-icons/filled/luggage.xml
new file mode 100644
index 0000000..a958af4
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17,6h-2V3c0,-0.55 -0.45,-1 -1,-1h-4C9.45,2 9,2.45 9,3v3H7C5.9,6 5,6.9 5,8v11c0,1.1 0.9,2 2,2c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1h6c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1c1.1,0 2,-0.9 2,-2V8C19,6.9 18.1,6 17,6zM9.5,18H8V9h1.5V18zM12.75,18h-1.5V9h1.5V18zM13.5,6h-3V3.5h3V6zM16,18h-1.5V9H16V18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/motion_photos_pause.xml b/ui/ui-material/icons/generator/raw-icons/filled/motion_photos_pause.xml
new file mode 100644
index 0000000..c3edf8d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/motion_photos_pause.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-1.19 0.22,-2.32 0.6,-3.38L4.48,9.3C4.17,10.14 4,11.05 4,12c0,4.41 3.59,8 8,8s8,-3.59 8,-8s-3.59,-8 -8,-8c-0.95,0 -1.85,0.17 -2.69,0.48L8.63,2.59C9.69,2.22 10.82,2 12,2C17.52,2 22,6.48 22,12zM5.5,4C4.67,4 4,4.67 4,5.5S4.67,7 5.5,7S7,6.33 7,5.5S6.33,4 5.5,4zM18,12c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6S18,8.69 18,12zM11,9H9v6h2V9zM15,9h-2v6h2V9z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/no_backpack.xml b/ui/ui-material/icons/generator/raw-icons/filled/no_backpack.xml
new file mode 100644
index 0000000..e53fe75
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/no_backpack.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.19,21.19L2.81,2.81L1.39,4.22l2.76,2.76C4.06,7.31 4,7.64 4,8v12c0,1.1 0.9,2 2,2h12c0.34,0 0.65,-0.09 0.93,-0.24l0.85,0.85L21.19,21.19zM6,14v-2h3.17l2,2H6zM14.83,12L6.98,4.15c0.01,0 0.01,-0.01 0.02,-0.01V2h3v2h4V2h3v2.14c1.72,0.45 3,2 3,3.86v9.17l-2,-2V12H14.83z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/no_luggage.xml b/ui/ui-material/icons/generator/raw-icons/filled/no_luggage.xml
new file mode 100644
index 0000000..ba4f667
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/no_luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12.75,9v0.92l1.75,1.75V9H16v4.17l3,3V8c0,-1.1 -0.9,-2 -2,-2h-2V3c0,-0.55 -0.45,-1 -1,-1h-4C9.45,2 9,2.45 9,3v3H8.83l3,3H12.75zM10.5,3.5h3V6h-3V3.5zM21.19,21.19L2.81,2.81L1.39,4.22l3.63,3.63C5.02,7.9 5,7.95 5,8v11c0,1.1 0.9,2 2,2c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1h6c0,0.55 0.45,1 1,1s1,-0.45 1,-1c0.34,0 0.65,-0.09 0.93,-0.24l1.85,1.85L21.19,21.19zM8,18v-7.17l1.5,1.5V18H8zM12.75,18h-1.5v-3.92l1.5,1.5V18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/outbond.xml b/ui/ui-material/icons/generator/raw-icons/filled/outbond.xml
new file mode 100644
index 0000000..2f90fb7
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/outbond.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM13.88,11.54l-4.96,4.96l-1.41,-1.41l4.96,-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/published_with_changes.xml b/ui/ui-material/icons/generator/raw-icons/filled/published_with_changes.xml
new file mode 100644
index 0000000..2c3609b2
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/published_with_changes.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17.66,9.53l-7.07,7.07l-4.24,-4.24l1.41,-1.41l2.83,2.83l5.66,-5.66L17.66,9.53zM4,12c0,-2.33 1.02,-4.42 2.62,-5.88L9,8.5v-6H3l2.2,2.2C3.24,6.52 2,9.11 2,12c0,5.19 3.95,9.45 9,9.95v-2.02C7.06,19.44 4,16.07 4,12zM22,12c0,-5.19 -3.95,-9.45 -9,-9.95v2.02c3.94,0.49 7,3.86 7,7.93c0,2.33 -1.02,4.42 -2.62,5.88L15,15.5v6h6l-2.2,-2.2C20.76,17.48 22,14.89 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/request_page.xml b/ui/ui-material/icons/generator/raw-icons/filled/request_page.xml
new file mode 100644
index 0000000..67b9090
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/request_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM15,11h-4v1h3c0.55,0 1,0.45 1,1v3c0,0.55 -0.45,1 -1,1h-1v1h-2v-1H9v-2h4v-1h-3c-0.55,0 -1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1h1V8h2v1h2V11z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/stacked_line_chart.xml b/ui/ui-material/icons/generator/raw-icons/filled/stacked_line_chart.xml
new file mode 100644
index 0000000..e776269
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/stacked_line_chart.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M2,19.99l7.5,-7.51l4,4l7.09,-7.97L22,9.92l-8.5,9.56l-4,-4l-6,6.01L2,19.99zM3.5,15.49l6,-6.01l4,4L22,3.92l-1.41,-1.41l-7.09,7.97l-4,-4L2,13.99L3.5,15.49z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/filled/unpublished.xml b/ui/ui-material/icons/generator/raw-icons/filled/unpublished.xml
new file mode 100644
index 0000000..1b8c174
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/filled/unpublished.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.19,21.19L2.81,2.81L1.39,4.22l2.27,2.27C2.61,8.07 2,9.96 2,12c0,5.52 4.48,10 10,10c2.04,0 3.93,-0.61 5.51,-1.66l2.27,2.27L21.19,21.19zM10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l0.18,-0.18l1.41,1.41L10.59,16.6zM13.59,10.76l-7.1,-7.1C8.07,2.61 9.96,2 12,2c5.52,0 10,4.48 10,10c0,2.04 -0.61,3.93 -1.66,5.51l-5.34,-5.34l2.65,-2.65l-1.41,-1.41L13.59,10.76z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/add_task.xml b/ui/ui-material/icons/generator/raw-icons/outlined/add_task.xml
new file mode 100644
index 0000000..295ddfa
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/add_task.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,5.18L10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l10,-10L22,5.18zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8c1.57,0 3.04,0.46 4.28,1.25l1.45,-1.45C16.1,2.67 14.13,2 12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.73,0 3.36,-0.44 4.78,-1.22l-1.5,-1.5C14.28,19.74 13.17,20 12,20zM19,15h-3v2h3v3h2v-3h3v-2h-3v-3h-2V15z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/book_online.xml b/ui/ui-material/icons/generator/raw-icons/outlined/book_online.xml
index 4447d8b..867dc35 100644
--- a/ui/ui-material/icons/generator/raw-icons/outlined/book_online.xml
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/book_online.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M19,8H5V6h14V8zM18,2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h8.92l-0.57,-2H5V10h14v4.35l2,0.57V6c0,-1.1 -0.9,-2 -2,-2h-1V2L18,2zM14,15l2,7l1.08,-2.51l3.22,3.22l1.41,-1.41l-3.22,-3.22L21,17L14,15L14,15zM12,17v-5H7v5H12z"/>
+      android:pathData="M17,4H7V3h10V4zM17,21H7v-1h10V21zM17,1H7C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1 17,1L17,1zM7,6h10v12H7V6zM16,11V9.14C16,8.51 15.55,8 15,8H9C8.45,8 8,8.51 8,9.14l0,1.96c0.55,0 1,0.45 1,1c0,0.55 -0.45,1 -1,1l0,1.76C8,15.49 8.45,16 9,16h6c0.55,0 1,-0.51 1,-1.14V13c-0.55,0 -1,-0.45 -1,-1C15,11.45 15.45,11 16,11zM12.5,14.5h-1v-1h1V14.5zM12.5,12.5h-1v-1h1V12.5zM12.5,10.5h-1v-1h1V10.5z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/contact_page.xml b/ui/ui-material/icons/generator/raw-icons/outlined/contact_page.xml
new file mode 100644
index 0000000..d0027b9
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/contact_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,4L18,8.83V20H6V4H13.17M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2L14,2zM12,14c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C10,13.1 10.9,14 12,14zM16,17.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C13.93,15.21 12.99,15 12,15c-0.99,0 -1.93,0.21 -2.78,0.58C8.48,15.9 8,16.62 8,17.43V18h8V17.43z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/details.xml b/ui/ui-material/icons/generator/raw-icons/outlined/details.xml
index 239c41e..91fe861 100644
--- a/ui/ui-material/icons/generator/raw-icons/outlined/details.xml
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/details.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M3,4l9,16 9,-16L3,4zM6.38,6h11.25L12,16 6.38,6z"/>
+      android:pathData="M12,3L2,21h20L12,3zM13,8.92L18.6,19H13V8.92zM11,8.92V19H5.4L11,8.92z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/disabled_by_default.xml b/ui/ui-material/icons/generator/raw-icons/outlined/disabled_by_default.xml
new file mode 100644
index 0000000..2e79702
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/disabled_by_default.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,19H5V5h14V19zM3,3v18h18V3H3zM17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/facebook.xml b/ui/ui-material/icons/generator/raw-icons/outlined/facebook.xml
new file mode 100644
index 0000000..308115a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/facebook.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,4.84 3.44,8.87 8,9.8V15H8v-3h2V9.5C10,7.57 11.57,6 13.5,6H16v3h-2c-0.55,0 -1,0.45 -1,1v2h3v3h-3v6.95C18.05,21.45 22,17.19 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/groups.xml b/ui/ui-material/icons/generator/raw-icons/outlined/groups.xml
new file mode 100644
index 0000000..e9db81e
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/groups.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M4,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C2,12.1 2.9,13 4,13zM5.13,14.1C4.76,14.04 4.39,14 4,14c-0.99,0 -1.93,0.21 -2.78,0.58C0.48,14.9 0,15.62 0,16.43V18l4.5,0v-1.61C4.5,15.56 4.73,14.78 5.13,14.1zM20,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C18,12.1 18.9,13 20,13zM24,16.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C21.93,14.21 20.99,14 20,14c-0.39,0 -0.76,0.04 -1.13,0.1c0.4,0.68 0.63,1.46 0.63,2.29V18l4.5,0V16.43zM16.24,13.65c-1.17,-0.52 -2.61,-0.9 -4.24,-0.9c-1.63,0 -3.07,0.39 -4.24,0.9C6.68,14.13 6,15.21 6,16.39V18h12v-1.61C18,15.21 17.32,14.13 16.24,13.65zM8.07,16c0.09,-0.23 0.13,-0.39 0.91,-0.69c0.97,-0.38 1.99,-0.56 3.02,-0.56s2.05,0.18 3.02,0.56c0.77,0.3 0.81,0.46 0.91,0.69H8.07zM12,8c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,8 12,8M12,6c-1.66,0 -3,1.34 -3,3c0,1.66 1.34,3 3,3s3,-1.34 3,-3C15,7.34 13.66,6 12,6L12,6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/luggage.xml b/ui/ui-material/icons/generator/raw-icons/outlined/luggage.xml
new file mode 100644
index 0000000..2c0c7b1
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M9.5,18H8V9h1.5V18zM12.75,18h-1.5V9h1.5V18zM16,18h-1.5V9H16V18zM17,6h-2V3c0,-0.55 -0.45,-1 -1,-1h-4C9.45,2 9,2.45 9,3v3H7C5.9,6 5,6.9 5,8v11c0,1.1 0.9,2 2,2c0,0.55 0.45,1 1,1s1,-0.45 1,-1h6c0,0.55 0.45,1 1,1s1,-0.45 1,-1c1.1,0 2,-0.9 2,-2V8C19,6.9 18.1,6 17,6zM10.5,3.5h3V6h-3V3.5zM17,19H7V8h10V19z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/motion_photos_pause.xml b/ui/ui-material/icons/generator/raw-icons/outlined/motion_photos_pause.xml
new file mode 100644
index 0000000..8f61f9a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/motion_photos_pause.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-1.19 0.22,-2.32 0.6,-3.38L4.48,9.3C4.17,10.14 4,11.05 4,12c0,4.41 3.59,8 8,8s8,-3.59 8,-8s-3.59,-8 -8,-8c-0.95,0 -1.85,0.17 -2.69,0.48L8.63,2.59C9.69,2.22 10.82,2 12,2C17.52,2 22,6.48 22,12zM5.5,7C6.33,7 7,6.33 7,5.5S6.33,4 5.5,4S4,4.67 4,5.5S4.67,7 5.5,7zM9,9v6h2V9H9zM13,9v6h2V9H13z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/no_backpack.xml b/ui/ui-material/icons/generator/raw-icons/outlined/no_backpack.xml
new file mode 100644
index 0000000..1b4a4be
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/no_backpack.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M6.98,4.15c0.01,0 0.01,-0.01 0.02,-0.01V2h3v2h4V2h3v2.14c1.72,0.45 3,2 3,3.86v9.17l-2,-2V8c0,-1.1 -0.9,-2 -2,-2H8.83L6.98,4.15zM14.83,12l1.67,1.67V12H14.83zM19.78,22.61l-0.85,-0.85C18.65,21.91 18.34,22 18,22H6c-1.1,0 -2,-0.9 -2,-2V8c0,-0.36 0.06,-0.69 0.15,-1.02L1.39,4.22l1.41,-1.41l18.38,18.38L19.78,22.61zM17.17,20l-6,-6H7.5v-2h1.67L6,8.83V20H17.17z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/no_luggage.xml b/ui/ui-material/icons/generator/raw-icons/outlined/no_luggage.xml
new file mode 100644
index 0000000..e01483b
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/no_luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M16,13.17l-1.5,-1.5V9H16V13.17zM19.78,22.61l-1.85,-1.85C17.65,20.91 17.34,21 17,21c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1H9c0,0.55 -0.45,1 -1,1c-0.55,0 -1,-0.45 -1,-1c-1.1,0 -2,-0.9 -2,-2V8c0,-0.05 0.02,-0.1 0.02,-0.15L1.39,4.22l1.41,-1.41l18.38,18.38L19.78,22.61zM16.17,19l-3.42,-3.42V18h-1.5v-3.92L9.5,12.33V18H8v-7.17l-1,-1V19H16.17zM12.75,9h-0.92l0.92,0.92V9zM19,8v8.17l-2,-2V8h-6.17L9.84,7.01L9,6.17V6V3c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1v3h2C18.1,6 19,6.9 19,8zM10.5,6h3V3.5h-3V6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/outbond.xml b/ui/ui-material/icons/generator/raw-icons/outlined/outbond.xml
new file mode 100644
index 0000000..4f21538
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/outbond.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13.88,11.54l-4.96,4.96l-1.41,-1.41l4.96,-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/published_with_changes.xml b/ui/ui-material/icons/generator/raw-icons/outlined/published_with_changes.xml
new file mode 100644
index 0000000..39535c9
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/published_with_changes.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M18.6,19.5H21v2h-6v-6h2v2.73c1.83,-1.47 3,-3.71 3,-6.23c0,-4.07 -3.06,-7.44 -7,-7.93V2.05c5.05,0.5 9,4.76 9,9.95C22,14.99 20.68,17.67 18.6,19.5zM4,12c0,-2.52 1.17,-4.77 3,-6.23V8.5h2v-6H3v2h2.4C3.32,6.33 2,9.01 2,12c0,5.19 3.95,9.45 9,9.95v-2.02C7.06,19.44 4,16.07 4,12zM16.24,8.11l-5.66,5.66l-2.83,-2.83l-1.41,1.41l4.24,4.24l7.07,-7.07L16.24,8.11z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/request_page.xml b/ui/ui-material/icons/generator/raw-icons/outlined/request_page.xml
new file mode 100644
index 0000000..9c1a16f
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/request_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,4L18,8.83V20H6V4H13.17M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2L14,2zM15,11h-4v1h3c0.55,0 1,0.45 1,1v3c0,0.55 -0.45,1 -1,1h-1v1h-2v-1H9v-2h4v-1h-3c-0.55,0 -1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1h1V8h2v1h2V11z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/stacked_line_chart.xml b/ui/ui-material/icons/generator/raw-icons/outlined/stacked_line_chart.xml
new file mode 100644
index 0000000..e776269
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/stacked_line_chart.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M2,19.99l7.5,-7.51l4,4l7.09,-7.97L22,9.92l-8.5,9.56l-4,-4l-6,6.01L2,19.99zM3.5,15.49l6,-6.01l4,4L22,3.92l-1.41,-1.41l-7.09,7.97l-4,-4L2,13.99L3.5,15.49z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/outlined/unpublished.xml b/ui/ui-material/icons/generator/raw-icons/outlined/unpublished.xml
new file mode 100644
index 0000000..2f72014
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/outlined/unpublished.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M7.94,5.12L6.49,3.66C8.07,2.61 9.96,2 12,2c5.52,0 10,4.48 10,10c0,2.04 -0.61,3.93 -1.66,5.51l-1.46,-1.46C19.59,14.86 20,13.48 20,12c0,-4.41 -3.59,-8 -8,-8C10.52,4 9.14,4.41 7.94,5.12zM17.66,9.53l-1.41,-1.41l-2.65,2.65l1.41,1.41L17.66,9.53zM19.78,22.61l-2.27,-2.27C15.93,21.39 14.04,22 12,22C6.48,22 2,17.52 2,12c0,-2.04 0.61,-3.93 1.66,-5.51L1.39,4.22l1.41,-1.41l18.38,18.38L19.78,22.61zM16.06,18.88l-3.88,-3.88l-1.59,1.59l-4.24,-4.24l1.41,-1.41l2.83,2.83l0.18,-0.18L5.12,7.94C4.41,9.14 4,10.52 4,12c0,4.41 3.59,8 8,8C13.48,20 14.86,19.59 16.06,18.88z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/add_task.xml b/ui/ui-material/icons/generator/raw-icons/rounded/add_task.xml
new file mode 100644
index 0000000..f5b221b
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/add_task.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.29,5.89l-10,10c-0.39,0.39 -1.02,0.39 -1.41,0l-2.83,-2.83c-0.39,-0.39 -0.39,-1.02 0,-1.41l0,0c0.39,-0.39 1.02,-0.39 1.41,0l2.12,2.12l9.29,-9.29c0.39,-0.39 1.02,-0.39 1.41,0v0C21.68,4.87 21.68,5.5 21.29,5.89zM12,20c-4.71,0 -8.48,-4.09 -7.95,-8.9c0.39,-3.52 3.12,-6.41 6.61,-6.99c1.81,-0.3 3.53,0.02 4.99,0.78c0.39,0.2 0.86,0.13 1.17,-0.18l0,0c0.48,-0.48 0.36,-1.29 -0.24,-1.6C15.11,2.36 13.45,1.95 11.68,2c-5.14,0.16 -9.41,4.34 -9.67,9.47C1.72,17.24 6.3,22 12,22c1.2,0 2.34,-0.21 3.41,-0.6c0.68,-0.25 0.87,-1.13 0.35,-1.65l0,0c-0.27,-0.27 -0.68,-0.37 -1.04,-0.23C13.87,19.83 12.95,20 12,20zM19,15h-2c-0.55,0 -1,0.45 -1,1v0c0,0.55 0.45,1 1,1h2v2c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1v-2h2c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1h-2v-2c0,-0.55 -0.45,-1 -1,-1h0c-0.55,0 -1,0.45 -1,1V15z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/book_online.xml b/ui/ui-material/icons/generator/raw-icons/rounded/book_online.xml
index 1a246f7..30a4ec7 100644
--- a/ui/ui-material/icons/generator/raw-icons/rounded/book_online.xml
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/book_online.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M13.35,20l0.57,2H5c-1.11,0 -2,-0.9 -2,-2L3.01,6c0,-1.1 0.88,-2 1.99,-2h1V3c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v1h8V3c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v1h1c1.1,0 2,0.9 2,2v8.92l-2,-0.57V10H5v10H13.35zM21,20.59l-2.51,-2.51l1.22,-0.52c0.43,-0.19 0.39,-0.81 -0.06,-0.94l-4.78,-1.37c-0.38,-0.11 -0.73,0.24 -0.62,0.62l1.37,4.78c0.13,0.45 0.75,0.49 0.94,0.06l0.52,-1.22L19.59,22c0.39,0.39 1.02,0.39 1.41,0l0,0C21.39,21.61 21.39,20.98 21,20.59zM12,14.5L12,14.5c0,-1.38 -1.12,-2.5 -2.5,-2.5h0C8.12,12 7,13.12 7,14.5v0C7,15.88 8.12,17 9.5,17h0C10.88,17 12,15.88 12,14.5z"/>
+      android:pathData="M17,1H7C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1 17,1L17,1zM7,6h10v12H7V6zM16,11V9.14C16,8.51 15.55,8 15,8H9C8.45,8 8,8.51 8,9.14l0,1.96c0.55,0 1,0.45 1,1c0,0.55 -0.45,1 -1,1l0,1.76C8,15.49 8.45,16 9,16h6c0.55,0 1,-0.51 1,-1.14V13c-0.55,0 -1,-0.45 -1,-1C15,11.45 15.45,11 16,11zM12,14.5L12,14.5c-0.28,0 -0.5,-0.22 -0.5,-0.5v0c0,-0.28 0.22,-0.5 0.5,-0.5h0c0.28,0 0.5,0.22 0.5,0.5v0C12.5,14.28 12.28,14.5 12,14.5zM12,12.5L12,12.5c-0.28,0 -0.5,-0.22 -0.5,-0.5v0c0,-0.28 0.22,-0.5 0.5,-0.5h0c0.28,0 0.5,0.22 0.5,0.5v0C12.5,12.28 12.28,12.5 12,12.5zM12,10.5L12,10.5c-0.28,0 -0.5,-0.22 -0.5,-0.5v0c0,-0.28 0.22,-0.5 0.5,-0.5h0c0.28,0 0.5,0.22 0.5,0.5v0C12.5,10.28 12.28,10.5 12,10.5z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/contact_page.xml b/ui/ui-material/icons/generator/raw-icons/rounded/contact_page.xml
new file mode 100644
index 0000000..4778e22
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/contact_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.83c0,-0.53 -0.21,-1.04 -0.59,-1.41l-4.83,-4.83C14.21,2.21 13.7,2 13.17,2zM12,10c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2C10,10.9 10.9,10 12,10zM16,18H8v-0.57c0,-0.81 0.48,-1.53 1.22,-1.85C10.07,15.21 11.01,15 12,15c0.99,0 1.93,0.21 2.78,0.58C15.52,15.9 16,16.62 16,17.43V18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/details.xml b/ui/ui-material/icons/generator/raw-icons/rounded/details.xml
index 98dc289..45636b7 100644
--- a/ui/ui-material/icons/generator/raw-icons/rounded/details.xml
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/details.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M3.84,5.49l7.29,12.96c0.38,0.68 1.36,0.68 1.74,0l7.29,-12.96c0.38,-0.67 -0.11,-1.49 -0.87,-1.49H4.71c-0.76,0 -1.25,0.82 -0.87,1.49zM6.38,6h11.25L12,16 6.38,6z"/>
+      android:pathData="M11.13,4.57l-8.3,14.94C2.46,20.18 2.94,21 3.7,21h16.6c0.76,0 1.24,-0.82 0.87,-1.49l-8.3,-14.94C12.49,3.89 11.51,3.89 11.13,4.57zM13,8.92L18.6,19H13V8.92zM11,8.92V19H5.4L11,8.92z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/disabled_by_default.xml b/ui/ui-material/icons/generator/raw-icons/rounded/disabled_by_default.xml
new file mode 100644
index 0000000..1962a17
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/disabled_by_default.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2H5C3.9,3 3,3.9 3,5zM16.3,16.29L16.3,16.29c-0.39,0.39 -1.02,0.39 -1.41,0L12,13.41l-2.89,2.89c-0.39,0.39 -1.02,0.39 -1.41,0l0,0c-0.39,-0.39 -0.39,-1.02 0,-1.41L10.59,12L7.7,9.11c-0.39,-0.39 -0.39,-1.02 0,-1.41l0,0c0.39,-0.39 1.02,-0.39 1.41,0L12,10.59l2.89,-2.88c0.39,-0.39 1.02,-0.39 1.41,0l0,0c0.39,0.39 0.39,1.02 0,1.41L13.41,12l2.89,2.88C16.68,15.27 16.68,15.91 16.3,16.29z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/facebook.xml b/ui/ui-material/icons/generator/raw-icons/rounded/facebook.xml
new file mode 100644
index 0000000..308115a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/facebook.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,4.84 3.44,8.87 8,9.8V15H8v-3h2V9.5C10,7.57 11.57,6 13.5,6H16v3h-2c-0.55,0 -1,0.45 -1,1v2h3v3h-3v6.95C18.05,21.45 22,17.19 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/groups.xml b/ui/ui-material/icons/generator/raw-icons/rounded/groups.xml
new file mode 100644
index 0000000..3cac006
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/groups.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,12.75c1.63,0 3.07,0.39 4.24,0.9c1.08,0.48 1.76,1.56 1.76,2.73L18,17c0,0.55 -0.45,1 -1,1H7c-0.55,0 -1,-0.45 -1,-1l0,-0.61c0,-1.18 0.68,-2.26 1.76,-2.73C8.93,13.14 10.37,12.75 12,12.75zM4,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C2,12.1 2.9,13 4,13zM5.13,14.1C4.76,14.04 4.39,14 4,14c-0.99,0 -1.93,0.21 -2.78,0.58C0.48,14.9 0,15.62 0,16.43L0,17c0,0.55 0.45,1 1,1l3.5,0v-1.61C4.5,15.56 4.73,14.78 5.13,14.1zM20,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C18,12.1 18.9,13 20,13zM24,16.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C21.93,14.21 20.99,14 20,14c-0.39,0 -0.76,0.04 -1.13,0.1c0.4,0.68 0.63,1.46 0.63,2.29V18l3.5,0c0.55,0 1,-0.45 1,-1L24,16.43zM12,6c1.66,0 3,1.34 3,3c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3C9,7.34 10.34,6 12,6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/luggage.xml b/ui/ui-material/icons/generator/raw-icons/rounded/luggage.xml
new file mode 100644
index 0000000..fe535e0
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17,6h-2V3c0,-0.55 -0.45,-1 -1,-1h-4C9.45,2 9,2.45 9,3v3H7C5.9,6 5,6.9 5,8v11c0,1.1 0.9,2 2,2c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1h6c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1c1.1,0 2,-0.9 2,-2V8C19,6.9 18.1,6 17,6zM8.75,18L8.75,18C8.34,18 8,17.66 8,17.25v-7.5C8,9.34 8.34,9 8.75,9h0C9.16,9 9.5,9.34 9.5,9.75v7.5C9.5,17.66 9.16,18 8.75,18zM12,18L12,18c-0.41,0 -0.75,-0.34 -0.75,-0.75v-7.5C11.25,9.34 11.59,9 12,9h0c0.41,0 0.75,0.34 0.75,0.75v7.5C12.75,17.66 12.41,18 12,18zM13.5,6h-3V3.5h3V6zM15.25,18L15.25,18c-0.41,0 -0.75,-0.34 -0.75,-0.75v-7.5C14.5,9.34 14.84,9 15.25,9h0C15.66,9 16,9.34 16,9.75v7.5C16,17.66 15.66,18 15.25,18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/motion_photos_pause.xml b/ui/ui-material/icons/generator/raw-icons/rounded/motion_photos_pause.xml
new file mode 100644
index 0000000..5a67151
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/motion_photos_pause.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.96,11.05c0.58,6.26 -4.64,11.48 -10.9,10.9c-4.43,-0.41 -8.12,-3.85 -8.9,-8.23C1.9,12.3 1.97,10.94 2.28,9.68c0.14,-0.58 0.76,-0.9 1.31,-0.7l0,0c0.47,0.17 0.75,0.67 0.63,1.16c-0.2,0.82 -0.27,1.7 -0.19,2.61c0.37,4.04 3.89,7.25 7.95,7.26c4.79,0.01 8.61,-4.21 7.94,-9.12c-0.51,-3.7 -3.66,-6.62 -7.39,-6.86c-0.83,-0.06 -1.63,0.02 -2.38,0.2C9.66,4.34 9.16,4.07 8.99,3.59l0,0c-0.2,-0.56 0.12,-1.17 0.69,-1.31c1.79,-0.43 3.75,-0.41 5.78,0.37C19.02,4 21.61,7.27 21.96,11.05zM5.5,4C4.67,4 4,4.67 4,5.5S4.67,7 5.5,7S7,6.33 7,5.5S6.33,4 5.5,4zM18,12c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6S18,8.69 18,12zM10,9L10,9c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1v-4C11,9.45 10.55,9 10,9zM14,9L14,9c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h0c0.55,0 1,-0.45 1,-1v-4C15,9.45 14.55,9 14,9z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/no_backpack.xml b/ui/ui-material/icons/generator/raw-icons/rounded/no_backpack.xml
new file mode 100644
index 0000000..85ec285
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/no_backpack.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M6.98,4.15c0.01,0 0.01,-0.01 0.02,-0.01V3.5C7,2.67 7.67,2 8.5,2S10,2.67 10,3.5V4h4V3.5C14,2.67 14.67,2 15.5,2S17,2.67 17,3.5v0.64c1.72,0.45 3,2 3,3.86v9.17l-2.03,-2.03C17.98,15.09 18,15.05 18,15v-2c0,-0.55 -0.45,-1 -1,-1h-2.17L6.98,4.15zM20.49,21.9c-0.39,0.39 -1.02,0.39 -1.41,0l-0.14,-0.14C18.65,21.91 18.34,22 18,22H6c-1.1,0 -2,-0.9 -2,-2V8c0,-0.36 0.06,-0.69 0.15,-1.02L2.1,4.93c-0.39,-0.39 -0.39,-1.02 0,-1.41c0.39,-0.39 1.02,-0.39 1.41,0l16.97,16.97C20.88,20.88 20.88,21.51 20.49,21.9zM11.17,14l-2,-2H7c-0.55,0 -1,0.45 -1,1c0,0.55 0.45,1 1,1H11.17z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/no_luggage.xml b/ui/ui-material/icons/generator/raw-icons/rounded/no_luggage.xml
new file mode 100644
index 0000000..ed28238
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/no_luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M20.49,20.49L3.51,3.51c-0.39,-0.39 -1.02,-0.39 -1.41,0c-0.39,0.39 -0.39,1.02 0,1.41l2.92,2.92C5.02,7.9 5,7.95 5,8v11c0,1.1 0.9,2 2,2c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1h6c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1c0.34,0 0.65,-0.09 0.93,-0.24l1.14,1.14c0.39,0.39 1.02,0.39 1.41,0C20.88,21.51 20.88,20.88 20.49,20.49zM8.75,18C8.34,18 8,17.66 8,17.25v-6.42l1.5,1.5v4.92C9.5,17.66 9.16,18 8.75,18zM12,18c-0.41,0 -0.75,-0.34 -0.75,-0.75v-3.17l1.5,1.5v1.67C12.75,17.66 12.41,18 12,18zM12,9c0.41,0 0.75,0.34 0.75,0.75v0.17l1.75,1.75V9.75C14.5,9.34 14.84,9 15.25,9S16,9.34 16,9.75v3.42l3,3V8c0,-1.1 -0.9,-2 -2,-2h-2V3c0,-0.55 -0.45,-1 -1,-1h-4C9.45,2 9,2.45 9,3v3H8.83l3.03,3.03C11.91,9.02 11.95,9 12,9zM10.5,3.5h3V6h-3V3.5z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/outbond.xml b/ui/ui-material/icons/generator/raw-icons/rounded/outbond.xml
new file mode 100644
index 0000000..5495c98
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/outbond.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM13.88,11.54l-4.25,4.25c-0.39,0.39 -1.02,0.39 -1.41,0l0,0c-0.39,-0.39 -0.39,-1.02 0,-1.41l4.25,-4.25L11.2,8.86C10.88,8.54 11.11,8 11.55,8l3.94,0c0.28,0 0.5,0.22 0.5,0.5l0,3.94c0,0.45 -0.54,0.67 -0.85,0.35L13.88,11.54z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/published_with_changes.xml b/ui/ui-material/icons/generator/raw-icons/rounded/published_with_changes.xml
new file mode 100644
index 0000000..72aa61d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/published_with_changes.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M16.95,10.23l-5.66,5.66c-0.39,0.39 -1.02,0.39 -1.41,0l-2.83,-2.83c-0.39,-0.39 -0.39,-1.02 0,-1.41l0,0c0.39,-0.39 1.02,-0.39 1.41,0l2.12,2.12l4.95,-4.95c0.39,-0.39 1.02,-0.39 1.41,0l0,0C17.34,9.21 17.34,9.84 16.95,10.23zM4,12c0,-2.33 1.02,-4.42 2.62,-5.88l1.53,1.53C8.46,7.96 9,7.74 9,7.29V3c0,-0.28 -0.22,-0.5 -0.5,-0.5H4.21c-0.45,0 -0.67,0.54 -0.35,0.85L5.2,4.7C3.24,6.52 2,9.11 2,12c0,4.75 3.32,8.73 7.76,9.75c0.63,0.14 1.24,-0.33 1.24,-0.98v0c0,-0.47 -0.33,-0.87 -0.79,-0.98C6.66,18.98 4,15.8 4,12zM22,12c0,-4.75 -3.32,-8.73 -7.76,-9.75C13.61,2.11 13,2.58 13,3.23v0c0,0.47 0.33,0.87 0.79,0.98C17.34,5.02 20,8.2 20,12c0,2.33 -1.02,4.42 -2.62,5.88l-1.53,-1.53C15.54,16.04 15,16.26 15,16.71V21c0,0.28 0.22,0.5 0.5,0.5h4.29c0.45,0 0.67,-0.54 0.35,-0.85L18.8,19.3C20.76,17.48 22,14.89 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/request_page.xml b/ui/ui-material/icons/generator/raw-icons/rounded/request_page.xml
new file mode 100644
index 0000000..089c81d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/request_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19.41,7.41l-4.83,-4.83C14.21,2.21 13.7,2 13.17,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.83C20,8.3 19.79,7.79 19.41,7.41zM14,12c0.55,0 1,0.45 1,1v3c0,0.55 -0.45,1 -1,1h-1c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1h-1c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1h3v-1h-3c-0.55,0 -1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1h1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1h1c0.55,0 1,0.45 1,1c0,0.55 -0.45,1 -1,1h-3v1H14z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/stacked_line_chart.xml b/ui/ui-material/icons/generator/raw-icons/rounded/stacked_line_chart.xml
new file mode 100644
index 0000000..a201b9c
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/stacked_line_chart.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M2.79,14.78L2.7,14.69c-0.39,-0.39 -0.39,-1.02 0,-1.41l6.09,-6.1c0.39,-0.39 1.02,-0.39 1.41,0l3.29,3.29l6.39,-7.18c0.38,-0.43 1.05,-0.44 1.45,-0.04l0,0c0.37,0.38 0.39,0.98 0.04,1.37l-7.17,8.07c-0.38,0.43 -1.04,0.45 -1.45,0.04L9.5,9.48l-5.3,5.3C3.82,15.17 3.18,15.17 2.79,14.78zM4.2,20.78l5.3,-5.3l3.25,3.25c0.41,0.41 1.07,0.39 1.45,-0.04l7.17,-8.07c0.35,-0.39 0.33,-0.99 -0.04,-1.37l0,0c-0.4,-0.4 -1.07,-0.39 -1.45,0.04l-6.39,7.18l-3.29,-3.29c-0.39,-0.39 -1.02,-0.39 -1.41,0l-6.09,6.1c-0.39,0.39 -0.39,1.02 0,1.41l0.09,0.09C3.18,21.17 3.82,21.17 4.2,20.78z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/rounded/unpublished.xml b/ui/ui-material/icons/generator/raw-icons/rounded/unpublished.xml
new file mode 100644
index 0000000..c404a65
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/rounded/unpublished.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M20.49,20.49L3.51,3.51c-0.39,-0.39 -1.02,-0.39 -1.41,0l0,0c-0.39,0.39 -0.39,1.02 0,1.41l1.56,1.56c-1.25,1.88 -1.88,4.21 -1.59,6.7c0.53,4.54 4.21,8.22 8.74,8.74c2.49,0.29 4.81,-0.34 6.7,-1.59l1.56,1.56c0.39,0.39 1.02,0.39 1.41,0l0,0C20.88,21.51 20.88,20.88 20.49,20.49zM9.88,15.89l-2.83,-2.83c-0.39,-0.39 -0.39,-1.02 0,-1.41l0,0c0.39,-0.39 1.02,-0.39 1.41,0l2.12,2.12l0.18,-0.18l1.41,1.41l-0.88,0.88C10.9,16.28 10.27,16.28 9.88,15.89zM13.59,10.76l-7.1,-7.1c1.88,-1.25 4.21,-1.88 6.7,-1.59c4.54,0.53 8.22,4.21 8.74,8.74c0.29,2.49 -0.34,4.82 -1.59,6.7l-5.34,-5.34l1.94,-1.94c0.39,-0.39 0.39,-1.02 0,-1.41v0c-0.39,-0.39 -1.02,-0.39 -1.41,0L13.59,10.76z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/add_task.xml b/ui/ui-material/icons/generator/raw-icons/sharp/add_task.xml
new file mode 100644
index 0000000..295ddfa
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/add_task.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,5.18L10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l10,-10L22,5.18zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8c1.57,0 3.04,0.46 4.28,1.25l1.45,-1.45C16.1,2.67 14.13,2 12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.73,0 3.36,-0.44 4.78,-1.22l-1.5,-1.5C14.28,19.74 13.17,20 12,20zM19,15h-3v2h3v3h2v-3h3v-2h-3v-3h-2V15z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/book_online.xml b/ui/ui-material/icons/generator/raw-icons/sharp/book_online.xml
index 6828dc8..386defb 100644
--- a/ui/ui-material/icons/generator/raw-icons/sharp/book_online.xml
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/book_online.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M13.35,20l0.57,2H3V4h3V2h2v2h8V2h2v2h3v10.92l-2,-0.57V10H5v10H13.35zM21.71,21.29l-3.22,-3.22L21,17l-7,-2l2,7l1.08,-2.51l3.22,3.22L21.71,21.29zM12,17v-5H7v5H12z"/>
+      android:pathData="M19,1H5v22h14V1zM7,18V6h10v12H7zM16,11l0,-3H8l0,3.1c0.55,0 1,0.45 1,1c0,0.55 -0.45,1 -1,1L8,16h8v-3c-0.55,0 -1,-0.45 -1,-1C15,11.45 15.45,11 16,11zM12.5,14.5h-1v-1h1V14.5zM12.5,12.5h-1v-1h1V12.5zM12.5,10.5h-1v-1h1V10.5z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/contact_page.xml b/ui/ui-material/icons/generator/raw-icons/sharp/contact_page.xml
new file mode 100644
index 0000000..d5c08ce
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/contact_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M14,2H4v20h16V8L14,2zM12,10c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2C10,10.9 10.9,10 12,10zM16,18H8v-0.57c0,-0.81 0.48,-1.53 1.22,-1.85C10.07,15.21 11.01,15 12,15c0.99,0 1.93,0.21 2.78,0.58C15.52,15.9 16,16.62 16,17.43V18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/details.xml b/ui/ui-material/icons/generator/raw-icons/sharp/details.xml
index 239c41e..91fe861 100644
--- a/ui/ui-material/icons/generator/raw-icons/sharp/details.xml
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/details.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M3,4l9,16 9,-16L3,4zM6.38,6h11.25L12,16 6.38,6z"/>
+      android:pathData="M12,3L2,21h20L12,3zM13,8.92L18.6,19H13V8.92zM11,8.92V19H5.4L11,8.92z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/disabled_by_default.xml b/ui/ui-material/icons/generator/raw-icons/sharp/disabled_by_default.xml
new file mode 100644
index 0000000..14e55ee
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/disabled_by_default.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M3,3v18h18V3H3zM17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/facebook.xml b/ui/ui-material/icons/generator/raw-icons/sharp/facebook.xml
new file mode 100644
index 0000000..308115a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/facebook.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,4.84 3.44,8.87 8,9.8V15H8v-3h2V9.5C10,7.57 11.57,6 13.5,6H16v3h-2c-0.55,0 -1,0.45 -1,1v2h3v3h-3v6.95C18.05,21.45 22,17.19 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/groups.xml b/ui/ui-material/icons/generator/raw-icons/sharp/groups.xml
new file mode 100644
index 0000000..a6df105
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/groups.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,12.75c1.63,0 3.07,0.39 4.24,0.9c1.08,0.48 1.76,1.56 1.76,2.73L18,18H6l0,-1.61c0,-1.18 0.68,-2.26 1.76,-2.73C8.93,13.14 10.37,12.75 12,12.75zM4,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C2,12.1 2.9,13 4,13zM5.13,14.1C4.76,14.04 4.39,14 4,14c-0.99,0 -1.93,0.21 -2.78,0.58C0.48,14.9 0,15.62 0,16.43V18l4.5,0v-1.61C4.5,15.56 4.73,14.78 5.13,14.1zM20,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C18,12.1 18.9,13 20,13zM24,16.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C21.93,14.21 20.99,14 20,14c-0.39,0 -0.76,0.04 -1.13,0.1c0.4,0.68 0.63,1.46 0.63,2.29V18l4.5,0V16.43zM12,6c1.66,0 3,1.34 3,3c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3C9,7.34 10.34,6 12,6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/luggage.xml b/ui/ui-material/icons/generator/raw-icons/sharp/luggage.xml
new file mode 100644
index 0000000..66daa82
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,6h-4V2H9v4H5v15h2c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1h6c0,0.55 0.45,1 1,1c0.55,0 1,-0.45 1,-1h2V6zM9.5,18H8V9h1.5V18zM12.75,18h-1.5V9h1.5V18zM13.5,6h-3V3.5h3V6zM16,18h-1.5V9H16V18z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_on.xml b/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_on.xml
index 3bbca24..bb44e7b 100644
--- a/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_on.xml
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_on.xml
@@ -6,5 +6,5 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M10,16.5v-9l6,4.5L10,16.5zM22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-1.19 0.22,-2.32 0.6,-3.38L4.48,9.3C4.17,10.14 4,11.05 4,12c0,4.41 3.59,8 8,8s8,-3.59 8,-8s-3.59,-8 -8,-8c-0.95,0 -1.85,0.17 -2.69,0.48L8.63,2.59C9.69,2.22 10.82,2 12,2C17.52,2 22,6.48 22,12zM5.5,4C4.67,4 4,4.67 4,5.5S4.67,7 5.5,7S7,6.33 7,5.5S6.33,4 5.5,4z"/>
+      android:pathData="M22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-1.19 0.22,-2.32 0.6,-3.38L4.48,9.3C4.17,10.14 4,11.05 4,12c0,4.41 3.59,8 8,8s8,-3.59 8,-8s-3.59,-8 -8,-8c-0.95,0 -1.85,0.17 -2.69,0.48L8.63,2.59C9.69,2.22 10.82,2 12,2C17.52,2 22,6.48 22,12zM5.5,4C4.67,4 4,4.67 4,5.5S4.67,7 5.5,7S7,6.33 7,5.5S6.33,4 5.5,4zM18,12c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6S18,8.69 18,12zM15,12l-5,-3v6L15,12z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_pause.xml b/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_pause.xml
new file mode 100644
index 0000000..c3edf8d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/motion_photos_pause.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-1.19 0.22,-2.32 0.6,-3.38L4.48,9.3C4.17,10.14 4,11.05 4,12c0,4.41 3.59,8 8,8s8,-3.59 8,-8s-3.59,-8 -8,-8c-0.95,0 -1.85,0.17 -2.69,0.48L8.63,2.59C9.69,2.22 10.82,2 12,2C17.52,2 22,6.48 22,12zM5.5,4C4.67,4 4,4.67 4,5.5S4.67,7 5.5,7S7,6.33 7,5.5S6.33,4 5.5,4zM18,12c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6S18,8.69 18,12zM11,9H9v6h2V9zM15,9h-2v6h2V9z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/no_backpack.xml b/ui/ui-material/icons/generator/raw-icons/sharp/no_backpack.xml
new file mode 100644
index 0000000..ae4ad32
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/no_backpack.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.19,21.19L2.81,2.81L1.39,4.22l2.76,2.76C4.06,7.31 4,7.64 4,8v14h15.17l0.61,0.61L21.19,21.19zM6,14v-2h3.17l2,2H6zM6.98,4.15c0.01,0 0.01,-0.01 0.02,-0.01V2h3v2h4V2h3v2.14c1.72,0.45 3,2 3,3.86v9.17l-2,-2V12h-3.17L6.98,4.15z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/no_luggage.xml b/ui/ui-material/icons/generator/raw-icons/sharp/no_luggage.xml
new file mode 100644
index 0000000..5f8286e
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/no_luggage.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12.75,9v0.92l1.75,1.75V9H16v4.17l3,3V6h-4V2H9v4H8.83l3,3H12.75zM10.5,3.5h3V6h-3V3.5zM21.19,21.19L2.81,2.81L1.39,4.22L5,7.83V21h2v1h2v-1h6v1h2v-1h1.17l1.61,1.61L21.19,21.19zM8,18v-7.17l1.5,1.5V18H8zM11.25,18v-3.92l1.5,1.5V18H11.25z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/outbond.xml b/ui/ui-material/icons/generator/raw-icons/sharp/outbond.xml
new file mode 100644
index 0000000..2f90fb7
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/outbond.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM13.88,11.54l-4.96,4.96l-1.41,-1.41l4.96,-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/published_with_changes.xml b/ui/ui-material/icons/generator/raw-icons/sharp/published_with_changes.xml
new file mode 100644
index 0000000..2c3609b2
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/published_with_changes.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17.66,9.53l-7.07,7.07l-4.24,-4.24l1.41,-1.41l2.83,2.83l5.66,-5.66L17.66,9.53zM4,12c0,-2.33 1.02,-4.42 2.62,-5.88L9,8.5v-6H3l2.2,2.2C3.24,6.52 2,9.11 2,12c0,5.19 3.95,9.45 9,9.95v-2.02C7.06,19.44 4,16.07 4,12zM22,12c0,-5.19 -3.95,-9.45 -9,-9.95v2.02c3.94,0.49 7,3.86 7,7.93c0,2.33 -1.02,4.42 -2.62,5.88L15,15.5v6h6l-2.2,-2.2C20.76,17.48 22,14.89 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/request_page.xml b/ui/ui-material/icons/generator/raw-icons/sharp/request_page.xml
new file mode 100644
index 0000000..f39100a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/request_page.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M14,2H4.01L4,22h16V8L14,2zM15,11h-4v1h4v5h-2v1h-2v-1H9v-2h4v-1H9V9h2V8h2v1h2V11z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/stacked_line_chart.xml b/ui/ui-material/icons/generator/raw-icons/sharp/stacked_line_chart.xml
new file mode 100644
index 0000000..e776269
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/stacked_line_chart.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M2,19.99l7.5,-7.51l4,4l7.09,-7.97L22,9.92l-8.5,9.56l-4,-4l-6,6.01L2,19.99zM3.5,15.49l6,-6.01l4,4L22,3.92l-1.41,-1.41l-7.09,7.97l-4,-4L2,13.99L3.5,15.49z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/sharp/unpublished.xml b/ui/ui-material/icons/generator/raw-icons/sharp/unpublished.xml
new file mode 100644
index 0000000..1b8c174
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/sharp/unpublished.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.19,21.19L2.81,2.81L1.39,4.22l2.27,2.27C2.61,8.07 2,9.96 2,12c0,5.52 4.48,10 10,10c2.04,0 3.93,-0.61 5.51,-1.66l2.27,2.27L21.19,21.19zM10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l0.18,-0.18l1.41,1.41L10.59,16.6zM13.59,10.76l-7.1,-7.1C8.07,2.61 9.96,2 12,2c5.52,0 10,4.48 10,10c0,2.04 -0.61,3.93 -1.66,5.51l-5.34,-5.34l2.65,-2.65l-1.41,-1.41L13.59,10.76z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/add_task.xml b/ui/ui-material/icons/generator/raw-icons/twotone/add_task.xml
new file mode 100644
index 0000000..295ddfa
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/add_task.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,5.18L10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l10,-10L22,5.18zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8c1.57,0 3.04,0.46 4.28,1.25l1.45,-1.45C16.1,2.67 14.13,2 12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.73,0 3.36,-0.44 4.78,-1.22l-1.5,-1.5C14.28,19.74 13.17,20 12,20zM19,15h-3v2h3v3h2v-3h3v-2h-3v-3h-2V15z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/book_online.xml b/ui/ui-material/icons/generator/raw-icons/twotone/book_online.xml
index 3900b45..4371c067 100644
--- a/ui/ui-material/icons/generator/raw-icons/twotone/book_online.xml
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/book_online.xml
@@ -6,10 +6,10 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M5,6h14v2h-14z"
+      android:pathData="M17,4H7V3h10V4zM17,21H7v-1h10V21z"
       android:strokeAlpha="0.3"
       android:fillAlpha="0.3"/>
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M19,8H5V6h14V8zM18,2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h8.92l-0.57,-2H5V10h14v4.35l2,0.57V6c0,-1.1 -0.9,-2 -2,-2h-1V2L18,2zM14,15l2,7l1.08,-2.51l3.22,3.22l1.41,-1.41l-3.22,-3.22L21,17L14,15L14,15zM12,17v-5H7v5H12z"/>
+      android:pathData="M17,4H7V3h10V4zM17,21H7v-1h10V21zM17,1H7C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1 17,1L17,1zM7,6h10v12H7V6zM16,11V9.14C16,8.51 15.55,8 15,8H9C8.45,8 8,8.51 8,9.14l0,1.96c0.55,0 1,0.45 1,1c0,0.55 -0.45,1 -1,1l0,1.76C8,15.49 8.45,16 9,16h6c0.55,0 1,-0.51 1,-1.14V13c-0.55,0 -1,-0.45 -1,-1C15,11.45 15.45,11 16,11zM12.5,14.5h-1v-1h1V14.5zM12.5,12.5h-1v-1h1V12.5zM12.5,10.5h-1v-1h1V10.5z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/contact_page.xml b/ui/ui-material/icons/generator/raw-icons/twotone/contact_page.xml
new file mode 100644
index 0000000..311b31e
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/contact_page.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,4L18,8.83V20H6V4H13.17M12,14c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C10,13.1 10.9,14 12,14zM16,17.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C13.93,15.21 12.99,15 12,15c-0.99,0 -1.93,0.21 -2.78,0.58C8.48,15.9 8,16.62 8,17.43V18h8V17.43z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,4L18,8.83V20H6V4H13.17M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2L14,2zM12,14c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C10,13.1 10.9,14 12,14zM16,17.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C13.93,15.21 12.99,15 12,15c-0.99,0 -1.93,0.21 -2.78,0.58C8.48,15.9 8,16.62 8,17.43V18h8V17.43z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/details.xml b/ui/ui-material/icons/generator/raw-icons/twotone/details.xml
index 3439051..6a8d0c2 100644
--- a/ui/ui-material/icons/generator/raw-icons/twotone/details.xml
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/details.xml
@@ -6,10 +6,10 @@
     android:tint="?attr/colorControlNormal">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M6.38,6L12,16l5.63,-10z"
+      android:pathData="M13,8.92L18.6,19H13V8.92zM11,8.92V19H5.4L11,8.92z"
       android:strokeAlpha="0.3"
       android:fillAlpha="0.3"/>
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M3,4l9,16 9,-16L3,4zM6.38,6h11.25L12,16 6.38,6z"/>
+      android:pathData="M12,3L2,21h20L12,3zM13,8.92L18.6,19H13V8.92zM11,8.92V19H5.4L11,8.92z"/>
 </vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/disabled_by_default.xml b/ui/ui-material/icons/generator/raw-icons/twotone/disabled_by_default.xml
new file mode 100644
index 0000000..af61a3d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/disabled_by_default.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M5,5v14h14V5H5zM17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,19H5V5h14V19zM3,3v18h18V3H3zM17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/facebook.xml b/ui/ui-material/icons/generator/raw-icons/twotone/facebook.xml
new file mode 100644
index 0000000..308115a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/facebook.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,4.84 3.44,8.87 8,9.8V15H8v-3h2V9.5C10,7.57 11.57,6 13.5,6H16v3h-2c-0.55,0 -1,0.45 -1,1v2h3v3h-3v6.95C18.05,21.45 22,17.19 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/groups.xml b/ui/ui-material/icons/generator/raw-icons/twotone/groups.xml
new file mode 100644
index 0000000..518b818
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/groups.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M8.07,16c0.09,-0.23 0.13,-0.39 0.91,-0.69c0.97,-0.38 1.99,-0.56 3.02,-0.56s2.05,0.18 3.02,0.56c0.77,0.3 0.81,0.46 0.91,0.69H8.07zM12,8c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,8 12,8"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M4,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C2,12.1 2.9,13 4,13zM5.13,14.1C4.76,14.04 4.39,14 4,14c-0.99,0 -1.93,0.21 -2.78,0.58C0.48,14.9 0,15.62 0,16.43V18l4.5,0v-1.61C4.5,15.56 4.73,14.78 5.13,14.1zM20,13c1.1,0 2,-0.9 2,-2c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2C18,12.1 18.9,13 20,13zM24,16.43c0,-0.81 -0.48,-1.53 -1.22,-1.85C21.93,14.21 20.99,14 20,14c-0.39,0 -0.76,0.04 -1.13,0.1c0.4,0.68 0.63,1.46 0.63,2.29V18l4.5,0V16.43zM16.24,13.65c-1.17,-0.52 -2.61,-0.9 -4.24,-0.9c-1.63,0 -3.07,0.39 -4.24,0.9C6.68,14.13 6,15.21 6,16.39V18h12v-1.61C18,15.21 17.32,14.13 16.24,13.65zM8.07,16c0.09,-0.23 0.13,-0.39 0.91,-0.69c0.97,-0.38 1.99,-0.56 3.02,-0.56s2.05,0.18 3.02,0.56c0.77,0.3 0.81,0.46 0.91,0.69H8.07zM12,8c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,8 12,8M12,6c-1.66,0 -3,1.34 -3,3c0,1.66 1.34,3 3,3s3,-1.34 3,-3C15,7.34 13.66,6 12,6L12,6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/luggage.xml b/ui/ui-material/icons/generator/raw-icons/twotone/luggage.xml
new file mode 100644
index 0000000..0c62e7d3
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/luggage.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M7,8v11h10V8H7zM9.5,18H8V9h1.5V18zM12.75,18h-1.5V9h1.5V18zM16,18h-1.5V9H16V18z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M9.5,18H8V9h1.5V18zM12.75,18h-1.5V9h1.5V18zM16,18h-1.5V9H16V18zM17,6h-2V3c0,-0.55 -0.45,-1 -1,-1h-4C9.45,2 9,2.45 9,3v3H7C5.9,6 5,6.9 5,8v11c0,1.1 0.9,2 2,2c0,0.55 0.45,1 1,1s1,-0.45 1,-1h6c0,0.55 0.45,1 1,1s1,-0.45 1,-1c1.1,0 2,-0.9 2,-2V8C19,6.9 18.1,6 17,6zM10.5,3.5h3V6h-3V3.5zM17,19H7V8h10V19z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/motion_photos_pause.xml b/ui/ui-material/icons/generator/raw-icons/twotone/motion_photos_pause.xml
new file mode 100644
index 0000000..8f61f9a
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/motion_photos_pause.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M22,12c0,5.52 -4.48,10 -10,10S2,17.52 2,12c0,-1.19 0.22,-2.32 0.6,-3.38L4.48,9.3C4.17,10.14 4,11.05 4,12c0,4.41 3.59,8 8,8s8,-3.59 8,-8s-3.59,-8 -8,-8c-0.95,0 -1.85,0.17 -2.69,0.48L8.63,2.59C9.69,2.22 10.82,2 12,2C17.52,2 22,6.48 22,12zM5.5,7C6.33,7 7,6.33 7,5.5S6.33,4 5.5,4S4,4.67 4,5.5S4.67,7 5.5,7zM9,9v6h2V9H9zM13,9v6h2V9H13z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/no_backpack.xml b/ui/ui-material/icons/generator/raw-icons/twotone/no_backpack.xml
new file mode 100644
index 0000000..b958eeb
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/no_backpack.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M18,15.17V8c0,-1.1 -0.9,-2 -2,-2H8.83l6,6h1.67v1.67L18,15.17zM17.17,20l-6,-6H7.5v-2h1.67L6,8.83V20H17.17z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M6.98,4.15c0.01,0 0.01,-0.01 0.02,-0.01V2h3v2h4V2h3v2.14c1.72,0.45 3,2 3,3.86v9.17l-2,-2V8c0,-1.1 -0.9,-2 -2,-2H8.83L6.98,4.15zM14.83,12l1.67,1.67V12H14.83zM19.78,22.61l-0.85,-0.85C18.65,21.91 18.34,22 18,22H6c-1.1,0 -2,-0.9 -2,-2V8c0,-0.36 0.06,-0.69 0.15,-1.02L1.39,4.22l1.41,-1.41l18.38,18.38L19.78,22.61zM17.17,20l-6,-6H7.5v-2h1.67L6,8.83V20H17.17z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/no_luggage.xml b/ui/ui-material/icons/generator/raw-icons/twotone/no_luggage.xml
new file mode 100644
index 0000000..d6de0bc
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/no_luggage.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M16.17,19l-3.42,-3.42V18h-1.5v-3.92L9.5,12.33V18H8v-7.17l-1,-1V19H16.17zM17,8v6.17l-1,-1V9h-1.5v2.67l-1.75,-1.75V9h-0.92l-1,-1H17z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M16,13.17l-1.5,-1.5V9H16V13.17zM19.78,22.61l-1.85,-1.85C17.65,20.91 17.34,21 17,21c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1H9c0,0.55 -0.45,1 -1,1c-0.55,0 -1,-0.45 -1,-1c-1.1,0 -2,-0.9 -2,-2V8c0,-0.05 0.02,-0.1 0.02,-0.15L1.39,4.22l1.41,-1.41l18.38,18.38L19.78,22.61zM16.17,19l-3.42,-3.42V18h-1.5v-3.92L9.5,12.33V18H8v-7.17l-1,-1V19H16.17zM12.75,9h-0.92l0.92,0.92V9zM19,8v8.17l-2,-2V8h-6.17L9.84,7.01L9,6.17V6V3c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1v3h2C18.1,6 19,6.9 19,8zM10.5,6h3V3.5h-3V6z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/outbond.xml b/ui/ui-material/icons/generator/raw-icons/twotone/outbond.xml
new file mode 100644
index 0000000..f7de78d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/outbond.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,4c-4.41,0 -8,3.59 -8,8c0,4.41 3.59,8 8,8s8,-3.59 8,-8C20,7.59 16.41,4 12,4zM13.88,11.54l-4.96,4.96l-1.41,-1.41l4.96,-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13.88,11.54l-4.96,4.96l-1.41,-1.41l4.96,-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/published_with_changes.xml b/ui/ui-material/icons/generator/raw-icons/twotone/published_with_changes.xml
new file mode 100644
index 0000000..2c3609b2
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/published_with_changes.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M17.66,9.53l-7.07,7.07l-4.24,-4.24l1.41,-1.41l2.83,2.83l5.66,-5.66L17.66,9.53zM4,12c0,-2.33 1.02,-4.42 2.62,-5.88L9,8.5v-6H3l2.2,2.2C3.24,6.52 2,9.11 2,12c0,5.19 3.95,9.45 9,9.95v-2.02C7.06,19.44 4,16.07 4,12zM22,12c0,-5.19 -3.95,-9.45 -9,-9.95v2.02c3.94,0.49 7,3.86 7,7.93c0,2.33 -1.02,4.42 -2.62,5.88L15,15.5v6h6l-2.2,-2.2C20.76,17.48 22,14.89 22,12z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/request_page.xml b/ui/ui-material/icons/generator/raw-icons/twotone/request_page.xml
new file mode 100644
index 0000000..d2bff0f
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/request_page.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,4H6v16h12V8.83L13.17,4zM15,11h-4v1h3c0.55,0 1,0.45 1,1v3c0,0.55 -0.45,1 -1,1h-1v1h-2v-1H9v-2h4v-1h-3c-0.55,0 -1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1h1V8h2v1h2V11z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.17,4L18,8.83V20H6V4H13.17M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2L14,2zM15,11h-4v1h3c0.55,0 1,0.45 1,1v3c0,0.55 -0.45,1 -1,1h-1v1h-2v-1H9v-2h4v-1h-3c-0.55,0 -1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1h1V8h2v1h2V11z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/stacked_line_chart.xml b/ui/ui-material/icons/generator/raw-icons/twotone/stacked_line_chart.xml
new file mode 100644
index 0000000..e776269
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/stacked_line_chart.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M2,19.99l7.5,-7.51l4,4l7.09,-7.97L22,9.92l-8.5,9.56l-4,-4l-6,6.01L2,19.99zM3.5,15.49l6,-6.01l4,4L22,3.92l-1.41,-1.41l-7.09,7.97l-4,-4L2,13.99L3.5,15.49z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/raw-icons/twotone/unpublished.xml b/ui/ui-material/icons/generator/raw-icons/twotone/unpublished.xml
new file mode 100644
index 0000000..075e05d
--- /dev/null
+++ b/ui/ui-material/icons/generator/raw-icons/twotone/unpublished.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M13.59,10.76l2.65,-2.65l1.41,1.41l-2.65,2.65l3.88,3.88C19.59,14.86 20,13.48 20,12c0,-4.41 -3.59,-8 -8,-8c-1.48,0 -2.86,0.41 -4.06,1.12L13.59,10.76zM17.66,9.53l-1.41,-1.41l-2.65,2.65l1.41,1.41L17.66,9.53zM16.06,18.88l-3.88,-3.88l-1.59,1.59l-4.24,-4.24l1.41,-1.41l2.83,2.83l0.18,-0.18L5.12,7.94C4.41,9.14 4,10.52 4,12c0,4.41 3.59,8 8,8C13.48,20 14.86,19.59 16.06,18.88z"
+      android:strokeAlpha="0.3"
+      android:fillAlpha="0.3"/>
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M7.94,5.12L6.49,3.66C8.07,2.61 9.96,2 12,2c5.52,0 10,4.48 10,10c0,2.04 -0.61,3.93 -1.66,5.51l-1.46,-1.46C19.59,14.86 20,13.48 20,12c0,-4.41 -3.59,-8 -8,-8C10.52,4 9.14,4.41 7.94,5.12zM17.66,9.53l-1.41,-1.41l-2.65,2.65l1.41,1.41L17.66,9.53zM19.78,22.61l-2.27,-2.27C15.93,21.39 14.04,22 12,22C6.48,22 2,17.52 2,12c0,-2.04 0.61,-3.93 1.66,-5.51L1.39,4.22l1.41,-1.41l18.38,18.38L19.78,22.61zM16.06,18.88l-3.88,-3.88l-1.59,1.59l-4.24,-4.24l1.41,-1.41l2.83,2.83l0.18,-0.18L5.12,7.94C4.41,9.14 4,10.52 4,12c0,4.41 3.59,8 8,8C13.48,20 14.86,19.59 16.06,18.88z"/>
+</vector>
diff --git a/ui/ui-material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt b/ui/ui-material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt
index 5b7d521..b2e1118 100644
--- a/ui/ui-material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt
+++ b/ui/ui-material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/VectorAssetGenerator.kt
@@ -25,6 +25,7 @@
 import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.buildCodeBlock
+import java.util.Locale
 
 /**
  * Generator for creating a Kotlin source file with a VectorAsset property for the given [vector],
@@ -53,7 +54,13 @@
         val iconsPackage = PackageNames.MaterialIconsPackage.packageName
         val themePackage = iconTheme.themePackageName
         val combinedPackageName = "$iconsPackage.$themePackage"
-        val backingProperty = backingProperty()
+        // Use a unique property name for the private backing property. This is because (as of
+        // Kotlin 1.4) each property with the same name will be considered as a possible candidate
+        // for resolution, regardless of the access modifier, so by using unique names we reduce
+        // the size from ~6000 to 1, and speed up compilation time for these icons.
+        @OptIn(ExperimentalStdlibApi::class)
+        val backingPropertyName = "_" + iconName.decapitalize(Locale.ROOT)
+        val backingProperty = backingProperty(name = backingPropertyName)
         return FileSpec.builder(
             packageName = combinedPackageName,
             fileName = iconName
@@ -74,14 +81,16 @@
      */
     private fun iconGetter(backingProperty: PropertySpec): FunSpec {
         return FunSpec.getterBuilder()
-            .addStatement("if (%N != null) return %N!!", backingProperty, backingProperty)
-            .addCode(
-                buildCodeBlock {
-                    beginControlFlow("%N = %M", backingProperty, MemberNames.MaterialIcon)
-                    vector.nodes.forEach { node -> addRecursively(node) }
-                    endControlFlow()
-                }
-            )
+            .addCode(buildCodeBlock {
+                beginControlFlow("if (%N != null)", backingProperty)
+                addStatement("return %N!!", backingProperty)
+                endControlFlow()
+            })
+            .addCode(buildCodeBlock {
+                beginControlFlow("%N = %M", backingProperty, MemberNames.MaterialIcon)
+                vector.nodes.forEach { node -> addRecursively(node) }
+                endControlFlow()
+            })
             .addStatement("return %N!!", backingProperty)
             .build()
     }
@@ -89,10 +98,12 @@
     /**
      * @return The private backing property that is used to cache the VectorAsset for a given
      * icon once created.
+     *
+     * @param name the name of this property
      */
-    private fun backingProperty(): PropertySpec {
+    private fun backingProperty(name: String): PropertySpec {
         val nullableVectorAsset = ClassNames.VectorAsset.copy(nullable = true)
-        return PropertySpec.builder(name = "icon", type = nullableVectorAsset)
+        return PropertySpec.builder(name = name, type = nullableVectorAsset)
             .mutable()
             .addModifiers(KModifier.PRIVATE)
             .initializer("null")
diff --git a/ui/ui-material/icons/generator/src/test/kotlin/androidx/compose/material/icons/generator/VectorAssetGeneratorTest.kt b/ui/ui-material/icons/generator/src/test/kotlin/androidx/compose/material/icons/generator/VectorAssetGeneratorTest.kt
index a1dece4..8a38551 100644
--- a/ui/ui-material/icons/generator/src/test/kotlin/androidx/compose/material/icons/generator/VectorAssetGeneratorTest.kt
+++ b/ui/ui-material/icons/generator/src/test/kotlin/androidx/compose/material/icons/generator/VectorAssetGeneratorTest.kt
@@ -72,8 +72,10 @@
 
     val Icons.Filled.TestVector: VectorAsset
         get() {
-            if (icon != null) return icon!!
-            icon = materialIcon {
+            if (_testVector != null) {
+                return _testVector!!
+            }
+            _testVector = materialIcon {
                 materialPath(fillAlpha = 0.8f) {
                     moveTo(20.0f, 10.0f)
                     lineToRelative(0.0f, 10.0f)
@@ -88,10 +90,10 @@
                     }
                 }
             }
-            return icon!!
+            return _testVector!!
         }
 
-    private var icon: VectorAsset? = null
+    private var _testVector: VectorAsset? = null
 
 """.trimIndent()
 
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
index a6f2b0e..6a469248 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
@@ -46,7 +46,6 @@
 import androidx.compose.ui.unit.height
 import androidx.compose.ui.unit.width
 import com.google.common.truth.Truth
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -223,7 +222,6 @@
     }
 
     @Test
-    @Ignore("b/162824105")
     fun bottomNavigation_selectNewItem() {
         composeTestRule.setMaterialContent {
             BottomNavigationSample()
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
index 50b314a..eb4705b 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
@@ -33,10 +33,11 @@
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.down
 import androidx.ui.test.isToggleable
+import androidx.ui.test.move
 import androidx.ui.test.onNode
 import androidx.ui.test.onNodeWithTag
-import androidx.ui.test.performClick
 import androidx.ui.test.performGesture
+import androidx.ui.test.up
 import androidx.ui.test.waitForIdle
 import org.junit.Rule
 import org.junit.Test
@@ -156,7 +157,9 @@
         composeTestRule.clockTestRule.pauseClock()
 
         onNode(isToggleable())
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         waitForIdle()
 
@@ -181,7 +184,9 @@
         composeTestRule.clockTestRule.pauseClock()
 
         onNode(isToggleable())
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         waitForIdle()
 
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/IconButtonTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/IconButtonTest.kt
index c54fb79..f3630b9 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/IconButtonTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/IconButtonTest.kt
@@ -35,7 +35,6 @@
 import androidx.ui.test.onNodeWithTag
 import androidx.ui.test.isToggleable
 import androidx.compose.ui.unit.dp
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -155,7 +154,6 @@
             .assertTopPositionInRootIsEqualTo((48.dp - height) / 2)
     }
 
-    @Ignore("b/162824105")
     @Test
     fun iconToggleButton_semantics() {
         composeTestRule.setMaterialContent {
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
index 05d9abf..0f56a91 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
@@ -33,10 +33,11 @@
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.down
 import androidx.ui.test.isInMutuallyExclusiveGroup
+import androidx.ui.test.move
 import androidx.ui.test.onNode
 import androidx.ui.test.onNodeWithTag
-import androidx.ui.test.performClick
 import androidx.ui.test.performGesture
+import androidx.ui.test.up
 import androidx.ui.test.waitForIdle
 import org.junit.Rule
 import org.junit.Test
@@ -128,7 +129,9 @@
         composeTestRule.clockTestRule.pauseClock()
 
         onNode(isInMutuallyExclusiveGroup())
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         waitForIdle()
 
@@ -152,7 +155,9 @@
         composeTestRule.clockTestRule.pauseClock()
 
         onNode(isInMutuallyExclusiveGroup())
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         waitForIdle()
 
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index 90a6eda..7851086 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -37,10 +37,11 @@
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.down
 import androidx.ui.test.isToggleable
+import androidx.ui.test.move
 import androidx.ui.test.onNode
 import androidx.ui.test.onNodeWithTag
-import androidx.ui.test.performClick
 import androidx.ui.test.performGesture
+import androidx.ui.test.up
 import androidx.ui.test.waitForIdle
 import org.junit.Rule
 import org.junit.Test
@@ -169,7 +170,9 @@
         composeTestRule.clockTestRule.pauseClock()
 
         onNode(isToggleable())
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         waitForIdle()
 
@@ -193,7 +196,9 @@
         composeTestRule.clockTestRule.pauseClock()
 
         onNode(isToggleable())
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         waitForIdle()
 
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index e733909..5832b9d 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -33,9 +33,13 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.ui.test.captureToBitmap
+import androidx.ui.test.center
 import androidx.ui.test.createComposeRule
+import androidx.ui.test.down
+import androidx.ui.test.move
 import androidx.ui.test.onNodeWithTag
-import androidx.ui.test.performClick
+import androidx.ui.test.performGesture
+import androidx.ui.test.up
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -96,7 +100,9 @@
         }
 
         onNodeWithTag(TextFieldTag)
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         assertAgainstGolden("outlined_textField_focused")
     }
@@ -116,7 +122,9 @@
         }
 
         onNodeWithTag(TextFieldTag)
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         assertAgainstGolden("outlined_textField_focused_rtl")
     }
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
index d43c6b3..b1c90fc 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
@@ -33,9 +33,13 @@
 import androidx.compose.ui.platform.LayoutDirectionAmbient
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.ui.test.captureToBitmap
+import androidx.ui.test.center
 import androidx.ui.test.createComposeRule
+import androidx.ui.test.down
+import androidx.ui.test.move
 import androidx.ui.test.onNodeWithTag
-import androidx.ui.test.performClick
+import androidx.ui.test.performGesture
+import androidx.ui.test.up
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -93,7 +97,9 @@
         }
 
         onNodeWithTag(TextFieldTag)
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         assertAgainstGolden("filled_textField_focused")
     }
@@ -112,7 +118,9 @@
         }
 
         onNodeWithTag(TextFieldTag)
-            .performClick()
+            // split click into (down) and (move, up) to enforce a composition in between
+            .performGesture { down(center) }
+            .performGesture { move(); up() }
 
         assertAgainstGolden("filled_textField_focused_rtl")
     }
diff --git a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index 7f2efb4..6ce3735 100644
--- a/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/ui/ui-material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -16,10 +16,7 @@
 
 package androidx.compose.material.textfield
 
-import android.content.Context
 import android.os.Build
-import android.view.View
-import android.view.inputmethod.InputMethodManager
 import androidx.compose.foundation.Box
 import androidx.compose.foundation.Text
 import androidx.compose.foundation.background
@@ -40,12 +37,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus
 import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
 import androidx.compose.ui.focusObserver
-import androidx.compose.ui.focusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
@@ -54,7 +48,6 @@
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.onPositioned
 import androidx.compose.ui.platform.TextInputServiceAmbient
-import androidx.compose.ui.platform.ViewAmbient
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.text.SoftwareKeyboardController
 import androidx.compose.ui.text.input.ImeAction
@@ -192,71 +185,6 @@
     }
 
     @Test
-    fun testTextField_showHideKeyboardBasedOnFocus() {
-        val parentFocusRequester = FocusRequester()
-        val focusRequester = FocusRequester()
-        lateinit var hostView: View
-        testRule.setMaterialContent {
-            hostView = ViewAmbient.current
-            Box {
-                TextField(
-                    modifier = Modifier
-                        .focusRequester(parentFocusRequester)
-                        .focus()
-                        .focusRequester(focusRequester)
-                        .testTag(TextfieldTag),
-                    value = "input",
-                    onValueChange = {},
-                    label = {}
-                )
-            }
-        }
-
-        // Shows keyboard when the text field is focused.
-        runOnIdle { focusRequester.requestFocus() }
-        runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isTrue() }
-
-            // Hides keyboard when the text field is not focused.
-        runOnIdle { parentFocusRequester.requestFocus() }
-        runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isFalse() }
-    }
-
-    @Test
-    fun testTextField_clickingOnTextAfterDismissingKeyboard_showHideKeyboard() {
-        val parentFocusRequester = FocusRequester()
-        val focusRequester = FocusRequester()
-        lateinit var softwareKeyboardController: SoftwareKeyboardController
-        lateinit var hostView: View
-        testRule.setMaterialContent {
-            hostView = ViewAmbient.current
-            Box {
-                TextField(
-                    modifier = Modifier
-                        .focusRequester(parentFocusRequester)
-                        .focus()
-                        .focusRequester(focusRequester)
-                        .testTag(TextfieldTag),
-                    value = "input",
-                    onValueChange = {},
-                    onTextInputStarted = { softwareKeyboardController = it },
-                    label = {}
-                )
-            }
-        }
-
-        // Shows keyboard when the text field is focused.
-        runOnIdle { focusRequester.requestFocus() }
-        runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isTrue() }
-
-        // Hide keyboard.
-        runOnIdle { softwareKeyboardController.hideSoftwareKeyboard() }
-
-        // Clicking on the text field shows the keyboard.
-        onNodeWithTag(TextfieldTag).performClick()
-        runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isTrue() }
-    }
-
-    @Test
     fun testTextField_labelPosition_initial_withDefaultHeight() {
         val labelSize = Ref<IntSize>()
         val labelPosition = Ref<Offset>()
@@ -876,12 +804,4 @@
         testRule.clockTestRule.pauseClock()
         testRule.clockTestRule.advanceClock(time)
     }
-
-    private val View.isSoftwareKeyboardShown: Boolean get() {
-        val inputMethodManager =
-            context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
-        // TODO(b/163742556): This is just a proxy for software keyboard visibility. Find a better
-        //  way to check if the software keyboard is shown.
-        return inputMethodManager.isAcceptingText()
-    }
 }
\ No newline at end of file
diff --git a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
index 0a62842..24923ab 100644
--- a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
+++ b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
@@ -30,6 +30,7 @@
 import androidx.compose.foundation.ContentColorAmbient
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.ProvideTextStyle
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.gestures.rememberScrollableController
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.padding
@@ -57,11 +58,9 @@
 import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
-import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.node.Ref
-import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.SoftwareKeyboardController
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.ImeAction
@@ -175,8 +174,15 @@
     val textFieldModifier = modifier
         .focusRequester(focusRequester)
         .focusObserver { isFocused = it.isFocused }
-        .tapGestureFilter { focusRequester.requestFocus() }
-        .semantics(mergeAllDescendants = true) {}
+        .clickable(indication = null) {
+            focusRequester.requestFocus()
+            // TODO(b/163109449): Showing and hiding keyboard should be handled by BaseTextField.
+            //  The requestFocus() call here should be enough to trigger the software keyboard.
+            //  Investiate why this is needed here. If it is really needed, instead of doing
+            //  this in the onClick callback, we should move this logic to the focusObserver
+            //  so that it can show or hide the keyboard based on the focus state.
+            keyboardController.value?.showSoftwareKeyboard()
+        }
 
     val emphasisLevels = EmphasisAmbient.current
 
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/BatchingTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/BatchingTest.kt
new file mode 100644
index 0000000..103d686
--- /dev/null
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/BatchingTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.ui.test.inputdispatcher
+
+import androidx.compose.ui.geometry.Offset
+import androidx.ui.test.util.expectError
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class BatchingTest : InputDispatcherTest() {
+
+    companion object {
+        private const val cannotEnqueueError = "Can't enqueue event \\(.*\\), " +
+                "events have already been \\(or are being\\) dispatched or disposed"
+        private const val cannotSendError = "Events have already " +
+                "been \\(or are being\\) dispatched or disposed"
+    }
+
+    /**
+     * Tests that enqueue doesn't send, send sends and dispose doesn't send anything else
+     *
+     * Happy path
+     */
+    @Test
+    fun enqueueSendDispose() {
+        subject.enqueueDown(0, Offset.Zero)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        assertThat(recorder.events).isEmpty()
+
+        subject.sendAllSynchronous()
+        assertThat(recorder.events).hasSize(3)
+
+        subject.dispose()
+        assertThat(recorder.events).hasSize(3)
+    }
+
+    /**
+     * Tests that enqueue doesn't send, send sends and subsequent enqueue fails
+     */
+    @Test
+    fun enqueueSendEnqueue() {
+        subject.enqueueDown(0, Offset.Zero)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        assertThat(recorder.events).isEmpty()
+
+        subject.sendAllSynchronous()
+        assertThat(recorder.events).hasSize(3)
+
+        expectError<IllegalStateException>(expectedMessage = cannotEnqueueError) {
+            subject.enqueueMove()
+        }
+        assertThat(recorder.events).hasSize(3)
+
+        // Do final check to see if the failed enqueue really didn't enqueue an event
+        expectError<IllegalStateException>(expectedMessage = cannotSendError) {
+            subject.sendAllSynchronous()
+        }
+        assertThat(recorder.events).hasSize(3)
+    }
+
+    /**
+     * Tests that enqueue doesn't send, send sends and subsequent send fails
+     */
+    @Test
+    fun enqueueSendSend() {
+        subject.enqueueDown(0, Offset.Zero)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        assertThat(recorder.events).isEmpty()
+
+        subject.sendAllSynchronous()
+        assertThat(recorder.events).hasSize(3)
+
+        expectError<IllegalStateException>(expectedMessage = cannotSendError) {
+            subject.sendAllSynchronous()
+        }
+        assertThat(recorder.events).hasSize(3)
+    }
+
+    /**
+     * Tests that enqueue doesn't send, dispose doesn't send anything and subsequent enqueue fails
+     */
+    @Test
+    fun enqueueDisposeEnqueue() {
+        subject.enqueueDown(0, Offset.Zero)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        assertThat(recorder.events).isEmpty()
+
+        subject.dispose()
+        assertThat(recorder.events).isEmpty()
+
+        expectError<IllegalStateException>(expectedMessage = cannotEnqueueError) {
+            subject.enqueueMove()
+        }
+        assertThat(recorder.events).isEmpty()
+
+        // Do final check to see if the failed enqueue really didn't enqueue an event
+        expectError<IllegalStateException>(expectedMessage = cannotSendError) {
+            subject.sendAllSynchronous()
+        }
+        assertThat(recorder.events).isEmpty()
+    }
+
+    /**
+     * Tests that enqueue doesn't send, dispose doesn't send anything and subsequent send fails
+     */
+    @Test
+    fun enqueueDisposeSend() {
+        subject.enqueueDown(0, Offset.Zero)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        assertThat(recorder.events).isEmpty()
+
+        subject.dispose()
+        assertThat(recorder.events).isEmpty()
+
+        expectError<IllegalStateException>(expectedMessage = cannotSendError) {
+            subject.sendAllSynchronous()
+        }
+        assertThat(recorder.events).isEmpty()
+    }
+
+    /**
+     * Tests that enqueue doesn't send, dispose doesn't send anything and subsequent dispose
+     * doesn't do anything either
+     */
+    @Test
+    fun enqueueDisposeDispose() {
+        subject.enqueueDown(0, Offset.Zero)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        assertThat(recorder.events).isEmpty()
+
+        subject.dispose()
+        assertThat(recorder.events).isEmpty()
+
+        subject.dispose()
+        assertThat(recorder.events).isEmpty()
+    }
+}
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/Common.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/Common.kt
deleted file mode 100644
index 4d073a7..0000000
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/Common.kt
+++ /dev/null
@@ -1,55 +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.ui.test.inputdispatcher
-
-import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher
-import androidx.ui.test.InputDispatcher
-import androidx.ui.test.android.AndroidInputDispatcher
-import com.google.common.truth.Truth.assertThat
-
-internal fun AndroidInputDispatcher.sendDownAndCheck(pointerId: Int, position: Offset) {
-    sendDown(pointerId, position)
-    assertThat(getCurrentPosition(pointerId)).isEqualTo(position)
-}
-
-internal fun AndroidInputDispatcher.movePointerAndCheck(pointerId: Int, position: Offset) {
-    movePointer(pointerId, position)
-    assertThat(getCurrentPosition(pointerId)).isEqualTo(position)
-}
-
-internal fun AndroidInputDispatcher.sendUpAndCheck(pointerId: Int, delay: Long? = null) {
-    if (delay != null) {
-        sendUp(pointerId, delay)
-    } else {
-        sendUp(pointerId)
-    }
-    assertThat(getCurrentPosition(pointerId)).isNull()
-}
-
-internal fun AndroidInputDispatcher.sendCancelAndCheck(delay: Long? = null) {
-    if (delay != null) {
-        sendCancel(delay)
-    } else {
-        sendCancel()
-    }
-    verifyNoGestureInProgress()
-}
-
-internal fun InputDispatcher.verifyNoGestureInProgress() {
-    assertThat((this as AndroidBaseInputDispatcher).isGestureInProgress).isFalse()
-}
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/DelayTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/DelayTest.kt
index dce7882..ab01f38 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/DelayTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/DelayTest.kt
@@ -18,30 +18,26 @@
 
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_UP
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.InputDispatcher
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
-import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.compose.ui.unit.Duration
 import androidx.compose.ui.unit.inMilliseconds
 import androidx.compose.ui.unit.milliseconds
+import androidx.test.filters.SmallTest
+import androidx.ui.test.InputDispatcher
+import androidx.ui.test.android.AndroidInputDispatcher
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 /**
- * Tests if [AndroidInputDispatcher.delay] works by performing three gestures with a delay in
- * between them. By varying the gestures and the delay, we test for lingering state problems.
+ * Tests if [AndroidInputDispatcher.enqueueDelay] works by performing three gestures with a
+ * delay in between them. By varying the gestures and the delay, we test for lingering state
+ * problems.
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class DelayTest(private val config: TestConfig) {
+class DelayTest(private val config: TestConfig) : InputDispatcherTest() {
     data class TestConfig(
         val firstDelay: Duration,
         val secondDelay: Duration,
@@ -51,8 +47,8 @@
     )
 
     enum class Gesture(internal val function: (InputDispatcher) -> Unit) {
-        Click({ it.sendClick(anyPosition) }),
-        Swipe({ it.sendSwipe(anyPosition, anyPosition, 107.milliseconds) })
+        Click({ it.enqueueClick(anyPosition) }),
+        Swipe({ it.enqueueSwipe(anyPosition, anyPosition, 107.milliseconds) })
     }
 
     companion object {
@@ -85,25 +81,15 @@
         }
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun testDelay() {
         // Perform two gestures with a delay in between
         config.firstGesture.function(subject)
-        subject.delay(config.firstDelay)
+        subject.enqueueDelay(config.firstDelay)
         config.secondGesture.function(subject)
-        subject.delay(config.secondDelay)
+        subject.enqueueDelay(config.secondDelay)
         config.thirdGesture.function(subject)
+        subject.sendAllSynchronous()
 
         // Check if the time between the gestures was exactly the delay
         val expectedFirstDelay = config.firstDelay.inMilliseconds()
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt
new file mode 100644
index 0000000..cbad2f5
--- /dev/null
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.ui.test.inputdispatcher
+
+import androidx.compose.ui.geometry.Offset
+import androidx.ui.test.AndroidBaseInputDispatcher
+import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.InputDispatcher
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.util.MotionEventRecorder
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Rule
+import org.junit.rules.TestRule
+
+open class InputDispatcherTest(eventPeriodOverride: Long? = null) {
+
+    @get:Rule
+    val inputDispatcherRule: TestRule = InputDispatcherTestRule(
+        disableDispatchInRealTime = true,
+        eventPeriodOverride = eventPeriodOverride
+    )
+
+    internal val recorder = MotionEventRecorder()
+    internal val subject = AndroidInputDispatcher(recorder::recordEvent)
+
+    @After
+    fun tearDown() {
+        // MotionEvents are still at the subject or in the recorder, but not both
+        subject.dispose()
+        recorder.disposeEvents()
+    }
+}
+
+internal fun AndroidInputDispatcher.generateDownAndCheck(pointerId: Int, position: Offset) {
+    enqueueDown(pointerId, position)
+    assertThat(getCurrentPosition(pointerId)).isEqualTo(position)
+}
+
+internal fun AndroidInputDispatcher.movePointerAndCheck(pointerId: Int, position: Offset) {
+    movePointer(pointerId, position)
+    assertThat(getCurrentPosition(pointerId)).isEqualTo(position)
+}
+
+internal fun AndroidInputDispatcher.generateUpAndCheck(pointerId: Int, delay: Long? = null) {
+    if (delay != null) {
+        enqueueUp(pointerId, delay)
+    } else {
+        enqueueUp(pointerId)
+    }
+    assertThat(getCurrentPosition(pointerId)).isNull()
+}
+
+internal fun AndroidInputDispatcher.generateCancelAndCheck(delay: Long? = null) {
+    if (delay != null) {
+        enqueueCancel(delay)
+    } else {
+        enqueueCancel()
+    }
+    verifyNoGestureInProgress()
+}
+
+internal fun InputDispatcher.verifyNoGestureInProgress() {
+    assertThat((this as AndroidBaseInputDispatcher).isGestureInProgress).isFalse()
+}
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/IsGestureInProgressTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/IsGestureInProgressTest.kt
index f619094..d3c7c42 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/IsGestureInProgressTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/IsGestureInProgressTest.kt
@@ -17,38 +17,29 @@
 package androidx.ui.test.inputdispatcher
 
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
-import androidx.ui.test.android.AndroidInputDispatcher
 import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 
-class IsGestureInProgressTest {
+class IsGestureInProgressTest : InputDispatcherTest() {
     companion object {
         private val anyPosition = Offset.Zero
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val subject = AndroidInputDispatcher {}
-
     @Test
     fun downUp() {
         assertThat(subject.isGestureInProgress).isFalse()
-        subject.sendDown(1, anyPosition)
+        subject.enqueueDown(1, anyPosition)
         assertThat(subject.isGestureInProgress).isTrue()
-        subject.sendUp(1)
+        subject.enqueueUp(1)
         assertThat(subject.isGestureInProgress).isFalse()
     }
 
     @Test
     fun downCancel() {
         assertThat(subject.isGestureInProgress).isFalse()
-        subject.sendDown(1, anyPosition)
+        subject.enqueueDown(1, anyPosition)
         assertThat(subject.isGestureInProgress).isTrue()
-        subject.sendCancel()
+        subject.enqueueCancel()
         assertThat(subject.isGestureInProgress).isFalse()
     }
-}
\ No newline at end of file
+}
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendCancelTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendCancelTest.kt
index 32a5a97..6ccb05d 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendCancelTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendCancelTest.kt
@@ -19,27 +19,22 @@
 import android.view.MotionEvent.ACTION_CANCEL
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_POINTER_DOWN
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
+import androidx.test.filters.SmallTest
 import androidx.ui.test.InputDispatcher.Companion.eventPeriod
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.expectError
 import androidx.ui.test.util.verifyEvent
 import androidx.ui.test.util.verifyPointer
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 
 /**
- * Tests if [AndroidInputDispatcher.sendCancel] works
+ * Tests if [AndroidInputDispatcher.enqueueCancel] works
  */
 @SmallTest
-class SendCancelTest {
+class SendCancelTest : InputDispatcherTest() {
     companion object {
         // pointerIds
         private const val pointer1 = 11
@@ -50,28 +45,18 @@
         private val position2_1 = Offset(21f, 21f)
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
-    private fun AndroidInputDispatcher.sendCancelAndCheckPointers(delay: Long? = null) {
-        sendCancelAndCheck(delay)
+    private fun AndroidInputDispatcher.generateCancelAndCheckPointers(delay: Long? = null) {
+        generateCancelAndCheck(delay)
         assertThat(getCurrentPosition(pointer1)).isNull()
         assertThat(getCurrentPosition(pointer2)).isNull()
     }
 
     @Test
     fun onePointer() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendCancelAndCheckPointers()
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateCancelAndCheckPointers()
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
         recorder.assertHasValidEventTimes()
 
         recorder.events.apply {
@@ -88,9 +73,10 @@
 
     @Test
     fun onePointerWithDelay() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendCancelAndCheckPointers(2 * eventPeriod)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateCancelAndCheckPointers(2 * eventPeriod)
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
         recorder.assertHasValidEventTimes()
 
         recorder.events.apply {
@@ -107,10 +93,11 @@
 
     @Test
     fun multiplePointers() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
-        subject.sendCancelAndCheckPointers()
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
+        subject.generateCancelAndCheckPointers()
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
         recorder.assertHasValidEventTimes()
 
         recorder.events.apply {
@@ -133,25 +120,25 @@
     @Test
     fun cancelWithoutDown() {
         expectError<IllegalStateException> {
-            subject.sendCancel()
+            subject.enqueueCancel()
         }
     }
 
     @Test
     fun cancelAfterUp() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendUp(pointer1)
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueUp(pointer1)
         expectError<IllegalStateException> {
-            subject.sendCancel()
+            subject.enqueueCancel()
         }
     }
 
     @Test
     fun cancelAfterCancel() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendCancel()
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueCancel()
         expectError<IllegalStateException> {
-            subject.sendCancel()
+            subject.enqueueCancel()
         }
     }
 }
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendClickTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendClickTest.kt
index cc8a23d..6e61559 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendClickTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendClickTest.kt
@@ -17,28 +17,23 @@
 package androidx.ui.test.inputdispatcher
 
 import android.view.MotionEvent
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
+import androidx.test.filters.SmallTest
 import androidx.ui.test.InputDispatcher.Companion.eventPeriod
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.verify
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 /**
- * Tests if [AndroidInputDispatcher.sendClick] works
+ * Tests if [AndroidInputDispatcher.enqueueClick] works
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class SendClickTest(config: TestConfig) {
+class SendClickTest(config: TestConfig) : InputDispatcherTest() {
     data class TestConfig(
         val x: Float,
         val y: Float
@@ -56,22 +51,12 @@
         }
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
     private val position = Offset(config.x, config.y)
 
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun testClick() {
-        subject.sendClick(position)
+        subject.enqueueClick(position)
+        subject.sendAllSynchronous()
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
             assertThat(size).isEqualTo(3)
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendDownTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendDownTest.kt
index c09bdb3..194ea6f 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendDownTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendDownTest.kt
@@ -20,27 +20,22 @@
 import android.view.MotionEvent.ACTION_MOVE
 import android.view.MotionEvent.ACTION_POINTER_DOWN
 import android.view.MotionEvent.ACTION_POINTER_UP
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
+import androidx.test.filters.SmallTest
 import androidx.ui.test.InputDispatcher.Companion.eventPeriod
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.expectError
 import androidx.ui.test.util.verifyEvent
 import androidx.ui.test.util.verifyPointer
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 
 /**
- * Tests if [AndroidInputDispatcher.sendDown] works
+ * Tests if [AndroidInputDispatcher.enqueueDown] works
  */
 @SmallTest
-class SendDownTest {
+class SendDownTest : InputDispatcherTest() {
     companion object {
         // Pointer ids
         private const val pointer1 = 11
@@ -58,20 +53,10 @@
         private val position1_2 = Offset(12f, 12f)
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun onePointer() {
-        subject.sendDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.sendAllSynchronous()
 
         val t = 0L
         recorder.assertHasValidEventTimes()
@@ -83,8 +68,9 @@
     @Test
     fun twoPointers_ascending() {
         // 2 fingers, sent in ascending order of pointerId (matters for actionIndex)
-        subject.sendDownAndCheck(pointer1, position1)
-        subject.sendDownAndCheck(pointer2, position2)
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer2, position2)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -103,8 +89,9 @@
     @Test
     fun twoPointers_descending() {
         // 2 fingers, sent in descending order of pointerId (matters for actionIndex)
-        subject.sendDownAndCheck(pointer2, position2)
-        subject.sendDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer2, position2)
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -124,10 +111,11 @@
     fun fourPointers() {
         // 4 fingers, sent in non-trivial order of pointerId (matters for actionIndex)
 
-        subject.sendDownAndCheck(pointer3, position3)
-        subject.sendDownAndCheck(pointer1, position1)
-        subject.sendDownAndCheck(pointer4, position4)
-        subject.sendDownAndCheck(pointer2, position2)
+        subject.generateDownAndCheck(pointer3, position3)
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer4, position4)
+        subject.generateDownAndCheck(pointer2, position2)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -159,14 +147,15 @@
         // 4 fingers, going down at different times
         // Each [sendMove] increases the time by 10 milliseconds
 
-        subject.sendDownAndCheck(pointer3, position3)
-        subject.sendMove()
-        subject.sendDownAndCheck(pointer1, position1)
-        subject.sendDownAndCheck(pointer2, position2)
-        subject.sendMove()
-        subject.sendMove()
-        subject.sendMove()
-        subject.sendDownAndCheck(pointer4, position4)
+        subject.generateDownAndCheck(pointer3, position3)
+        subject.enqueueMove()
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer2, position2)
+        subject.enqueueMove()
+        subject.enqueueMove()
+        subject.enqueueMove()
+        subject.generateDownAndCheck(pointer4, position4)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -210,12 +199,13 @@
         // 3 fingers, where the 1st finger goes up before the 3rd finger goes down (no overlap)
         // Each [sendMove] increases the time by 10 milliseconds
 
-        subject.sendDownAndCheck(pointer1, position1)
-        subject.sendDownAndCheck(pointer2, position2)
-        subject.sendMove()
-        subject.sendUpAndCheck(pointer1)
-        subject.sendMove()
-        subject.sendDownAndCheck(pointer3, position3)
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer2, position2)
+        subject.enqueueMove()
+        subject.generateUpAndCheck(pointer1)
+        subject.enqueueMove()
+        subject.generateDownAndCheck(pointer3, position3)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -254,12 +244,13 @@
         // fingers reuses the pointerId of finger 1
         // Each [sendMove] increases the time by 10 milliseconds
 
-        subject.sendDownAndCheck(pointer1, position1)
-        subject.sendDownAndCheck(pointer2, position2)
-        subject.sendMove()
-        subject.sendUpAndCheck(pointer1)
-        subject.sendMove()
-        subject.sendDownAndCheck(pointer1, position1_2)
+        subject.generateDownAndCheck(pointer1, position1)
+        subject.generateDownAndCheck(pointer2, position2)
+        subject.enqueueMove()
+        subject.generateUpAndCheck(pointer1)
+        subject.enqueueMove()
+        subject.generateDownAndCheck(pointer1, position1_2)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -294,9 +285,9 @@
 
     @Test
     fun downAfterDown() {
-        subject.sendDown(pointer1, position1)
+        subject.enqueueDown(pointer1, position1)
         expectError<IllegalArgumentException> {
-            subject.sendDown(pointer1, position2)
+            subject.enqueueDown(pointer1, position2)
         }
     }
 }
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendMoveTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendMoveTest.kt
index dfd9056..cc843fe 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendMoveTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendMoveTest.kt
@@ -21,27 +21,22 @@
 import android.view.MotionEvent.ACTION_MOVE
 import android.view.MotionEvent.ACTION_POINTER_DOWN
 import android.view.MotionEvent.ACTION_POINTER_UP
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
+import androidx.test.filters.SmallTest
 import androidx.ui.test.InputDispatcher.Companion.eventPeriod
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.expectError
 import androidx.ui.test.util.verifyEvent
 import androidx.ui.test.util.verifyPointer
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 
 /**
- * Tests if [AndroidInputDispatcher.movePointer] and [AndroidInputDispatcher.sendMove] work
+ * Tests if [AndroidInputDispatcher.movePointer] and [AndroidInputDispatcher.enqueueMove] work
  */
 @SmallTest
-class SendMoveTest {
+class SendMoveTest : InputDispatcherTest() {
     companion object {
         // pointerIds
         private const val pointer1 = 11
@@ -59,19 +54,8 @@
         private val position1_3 = Offset(13f, 13f)
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
-    private fun AndroidInputDispatcher.sendCancelAndCheckPointers() {
-        sendCancelAndCheck()
+    private fun AndroidInputDispatcher.generateCancelAndCheckPointers() {
+        generateCancelAndCheck()
         assertThat(getCurrentPosition(pointer1)).isNull()
         assertThat(getCurrentPosition(pointer2)).isNull()
         assertThat(getCurrentPosition(pointer3)).isNull()
@@ -79,9 +63,10 @@
 
     @Test
     fun onePointer() {
-        subject.sendDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
         subject.movePointerAndCheck(pointer1, position1_2)
-        subject.sendMove()
+        subject.enqueueMove()
+        subject.sendAllSynchronous()
 
         var t = 0L
         recorder.assertHasValidEventTimes()
@@ -96,9 +81,10 @@
 
     @Test
     fun onePointerWithDelay() {
-        subject.sendDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
         subject.movePointerAndCheck(pointer1, position1_2)
-        subject.sendMove(2 * eventPeriod)
+        subject.enqueueMove(2 * eventPeriod)
+        subject.sendAllSynchronous()
 
         var t = 0L
         recorder.assertHasValidEventTimes()
@@ -114,12 +100,13 @@
     @Test
     fun twoPointers_downDownMoveMove() {
         // 2 fingers, both go down before they move
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer1, position1_2)
-        subject.sendMove()
+        subject.enqueueMove()
         subject.movePointerAndCheck(pointer2, position2_2)
-        subject.sendMove()
+        subject.enqueueMove()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -148,12 +135,13 @@
     @Test
     fun twoPointers_downMoveDownMove() {
         // 2 fingers, 1st finger moves before 2nd finger goes down and moves
-        subject.sendDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
         subject.movePointerAndCheck(pointer1, position1_2)
-        subject.sendMove()
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.enqueueMove()
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer2, position2_2)
-        subject.sendMove()
+        subject.enqueueMove()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -181,11 +169,12 @@
     @Test
     fun movePointer_oneMovePerPointer() {
         // 2 fingers, use [movePointer] and [sendMove]
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer1, position1_2)
         subject.movePointerAndCheck(pointer2, position2_2)
-        subject.sendMove()
+        subject.enqueueMove()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -209,11 +198,12 @@
     @Test
     fun movePointer_multipleMovesPerPointer() {
         // 2 fingers, do several [movePointer]s and then [sendMove]
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer1, position1_2)
         subject.movePointerAndCheck(pointer1, position1_3)
-        subject.sendMove()
+        subject.enqueueMove()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -237,9 +227,10 @@
     @Test
     fun sendMoveWithoutMovePointer() {
         // 2 fingers, do [sendMove] without [movePointer]
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
-        subject.sendMove()
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
+        subject.enqueueMove()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -263,11 +254,12 @@
     @Test
     fun downFlushesPointerMovement() {
         // Movement from [movePointer] that hasn't been sent will be sent when sending DOWN
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer1, position1_2)
         subject.movePointerAndCheck(pointer1, position1_3)
-        subject.sendDownAndCheck(pointer3, position3_1)
+        subject.generateDownAndCheck(pointer3, position3_1)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -296,11 +288,12 @@
     @Test
     fun upFlushesPointerMovement() {
         // Movement from [movePointer] that hasn't been sent will be sent when sending UP
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer1, position1_2)
         subject.movePointerAndCheck(pointer1, position1_3)
-        subject.sendUpAndCheck(pointer1)
+        subject.generateUpAndCheck(pointer1)
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -329,11 +322,12 @@
     fun cancelDoesNotFlushPointerMovement() {
         // 2 fingers, both with pending movement.
         // CANCEL doesn't force a MOVE, but _does_ reflect the latest positions
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
         subject.movePointerAndCheck(pointer1, position1_2)
         subject.movePointerAndCheck(pointer2, position2_2)
-        subject.sendCancelAndCheckPointers()
+        subject.generateCancelAndCheckPointers()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -362,7 +356,7 @@
 
     @Test
     fun movePointerWrongPointerId() {
-        subject.sendDown(pointer1, position1_1)
+        subject.enqueueDown(pointer1, position1_1)
         expectError<IllegalArgumentException> {
             subject.movePointer(pointer2, position1_2)
         }
@@ -370,8 +364,8 @@
 
     @Test
     fun movePointerAfterUp() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendUp(pointer1)
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueUp(pointer1)
         expectError<IllegalStateException> {
             subject.movePointer(pointer1, position1_2)
         }
@@ -379,8 +373,8 @@
 
     @Test
     fun movePointerAfterCancel() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendCancel()
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueCancel()
         expectError<IllegalStateException> {
             subject.movePointer(pointer1, position1_2)
         }
@@ -389,25 +383,25 @@
     @Test
     fun sendMoveWithoutDown() {
         expectError<IllegalStateException> {
-            subject.sendMove()
+            subject.enqueueMove()
         }
     }
 
     @Test
     fun sendMoveAfterUp() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendUp(pointer1)
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueUp(pointer1)
         expectError<IllegalStateException> {
-            subject.sendMove()
+            subject.enqueueMove()
         }
     }
 
     @Test
     fun sendMoveAfterCancel() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendCancel()
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueCancel()
         expectError<IllegalStateException> {
-            subject.sendMove()
+            subject.enqueueMove()
         }
     }
 }
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeLineTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeLineTest.kt
index f404c9f..f584670 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeLineTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeLineTest.kt
@@ -17,33 +17,28 @@
 package androidx.ui.test.inputdispatcher
 
 import android.view.MotionEvent
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.compose.ui.unit.milliseconds
+import androidx.test.filters.SmallTest
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.isMonotonicBetween
 import androidx.ui.test.util.moveEvents
 import androidx.ui.test.util.splitsDurationEquallyInto
 import androidx.ui.test.util.verify
-import androidx.compose.ui.unit.milliseconds
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import kotlin.math.max
 
 /**
- * Tests if the [AndroidInputDispatcher.sendSwipe] gesture works when specifying the gesture as a
+ * Tests if the [AndroidInputDispatcher.enqueueSwipe] gesture works when specifying the gesture as a
  * line between two positions
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class SendSwipeLineTest(private val config: TestConfig) {
+class SendSwipeLineTest(private val config: TestConfig) : InputDispatcherTest(config.eventPeriod) {
     data class TestConfig(
         val duration: Long,
         val eventPeriod: Long
@@ -64,26 +59,14 @@
         }
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(
-        disableDispatchInRealTime = true,
-        eventPeriodOverride = config.eventPeriod
-    )
-
     private val duration get() = config.duration
     private val eventPeriod = config.eventPeriod
 
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun swipeByLine() {
-        subject.sendSwipe(start, end, duration.milliseconds)
+        subject.enqueueSwipe(start, end, duration.milliseconds)
+        subject.sendAllSynchronous()
+
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
             val expectedMoveEvents = max(1, duration / eventPeriod).toInt()
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithDurationTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithDurationTest.kt
index a17341d..24bbb15 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithDurationTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithDurationTest.kt
@@ -19,34 +19,30 @@
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_MOVE
 import android.view.MotionEvent.ACTION_UP
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
-import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
-import androidx.ui.test.util.assertHasValidEventTimes
-import androidx.ui.test.util.verify
 import androidx.compose.ui.unit.Duration
 import androidx.compose.ui.unit.inMilliseconds
 import androidx.compose.ui.unit.milliseconds
+import androidx.test.filters.SmallTest
+import androidx.ui.test.InputDispatcher
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.util.assertHasValidEventTimes
+import androidx.ui.test.util.verify
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 /**
- * Tests if the [AndroidInputDispatcher.sendSwipe] gesture works when specifying the gesture as a
+ * Tests if the [AndroidInputDispatcher.enqueueSwipe] gesture works when specifying the gesture as a
  * function between two positions. Verifies if the generated MotionEvents for a gesture with a
  * given duration have the expected timestamps. The timestamps should divide the duration as
- * equally as possible with as close to [AndroidInputDispatcher.eventPeriod] between each
+ * equally as possible with as close to [InputDispatcher.eventPeriod] between each
  * successive event as possible.
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class SendSwipeWithDurationTest(private val config: TestConfig) {
+class SendSwipeWithDurationTest(private val config: TestConfig) : InputDispatcherTest() {
     data class TestConfig(
         val duration: Duration,
         val expectedTimestamps: List<Long>
@@ -94,21 +90,11 @@
         }
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun swipeWithDuration() {
         // Given a swipe with a given duration
-        subject.sendSwipe(curve = curve, duration = config.duration)
+        subject.enqueueSwipe(curve = curve, duration = config.duration)
+        subject.sendAllSynchronous()
 
         // then
         val expectedNumberOfMoveEvents = config.expectedTimestamps.size
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesAndEventPeriodTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesAndEventPeriodTest.kt
index 080a5c0..8881cd8 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesAndEventPeriodTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesAndEventPeriodTest.kt
@@ -18,11 +18,11 @@
 
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_UP
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.compose.ui.unit.Duration
+import androidx.compose.ui.unit.inMilliseconds
+import androidx.test.filters.SmallTest
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.between
 import androidx.ui.test.util.moveEvents
@@ -30,19 +30,14 @@
 import androidx.ui.test.util.relativeTime
 import androidx.ui.test.util.splitsDurationEquallyInto
 import androidx.ui.test.util.verify
-import androidx.compose.ui.unit.Duration
-import androidx.compose.ui.unit.inMilliseconds
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 /**
- * Tests if the [AndroidInputDispatcher.sendSwipe] gesture works when specifying the gesture as a
+ * Tests if the [AndroidInputDispatcher.enqueueSwipe] gesture works when specifying the gesture as a
  * function between two positions. Verifies if the generated MotionEvents for a gesture with a
  * given duration and a set of keyTimes have the expected timestamps, if the event period would
  * be different. This is not a situation that can occur in practice, but is necessary to test to
@@ -57,7 +52,9 @@
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class SendSwipeWithKeyTimesAndEventPeriodTest(private val config: TestConfig) {
+class SendSwipeWithKeyTimesAndEventPeriodTest(
+    private val config: TestConfig
+) : InputDispatcherTest(config.eventPeriod) {
     data class TestConfig(
         val duration: Duration,
         val keyTimes: List<Long>,
@@ -92,19 +89,10 @@
         }
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(
-        disableDispatchInRealTime = true,
-        eventPeriodOverride = config.eventPeriod
-    )
-
     private val duration get() = config.duration
     private val keyTimes get() = config.keyTimes
     private val eventPeriod = config.eventPeriod
 
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
     @Before
     fun setUp() {
         require(config.keyTimes.distinct() == config.keyTimes.distinct().sorted()) {
@@ -112,15 +100,11 @@
         }
     }
 
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun swipeWithKeyTimesAndEventPeriod() {
         // Given a specific eventPeriod and a swipe with a given duration and set of keyTimes
-        subject.sendSwipe(curve = curve, duration = duration, keyTimes = keyTimes)
+        subject.enqueueSwipe(curve = curve, duration = duration, keyTimes = keyTimes)
+        subject.sendAllSynchronous()
 
         // then
         recorder.assertHasValidEventTimes()
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesTest.kt
index 28d0797..26835d0 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendSwipeWithKeyTimesTest.kt
@@ -19,34 +19,30 @@
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_MOVE
 import android.view.MotionEvent.ACTION_UP
-import androidx.test.filters.SmallTest
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
-import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
-import androidx.ui.test.util.assertHasValidEventTimes
-import androidx.ui.test.util.verify
 import androidx.compose.ui.unit.Duration
 import androidx.compose.ui.unit.inMilliseconds
 import androidx.compose.ui.unit.milliseconds
+import androidx.test.filters.SmallTest
+import androidx.ui.test.InputDispatcher
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.util.assertHasValidEventTimes
+import androidx.ui.test.util.verify
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 /**
- * Tests if the [AndroidInputDispatcher.sendSwipe] gesture works when specifying the gesture as a
+ * Tests if the [AndroidInputDispatcher.enqueueSwipe] gesture works when specifying the gesture as a
  * function between two positions. Verifies if the generated MotionEvents for a gesture with a
  * given duration and a set of keyTimes have the expected timestamps. The timestamps should
  * include all keyTimes, and divide the duration between those keyTimes as equally as possible
- * with as close to [AndroidInputDispatcher.eventPeriod] between each successive event as possible.
+ * with as close to [InputDispatcher.eventPeriod] between each successive event as possible.
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class SendSwipeWithKeyTimesTest(private val config: TestConfig) {
+class SendSwipeWithKeyTimesTest(private val config: TestConfig) : InputDispatcherTest() {
     data class TestConfig(
         val duration: Duration,
         val keyTimes: List<Long>,
@@ -84,12 +80,6 @@
         }
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
     @Before
     fun setUp() {
         require(config.keyTimes.distinct() == config.keyTimes.distinct().sorted()) {
@@ -97,15 +87,11 @@
         }
     }
 
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun swipeWithKeyTimes() {
         // Given a swipe with a given duration and set of keyTimes
-        subject.sendSwipe(curve = curve, duration = config.duration, keyTimes = config.keyTimes)
+        subject.enqueueSwipe(curve = curve, duration = config.duration, keyTimes = config.keyTimes)
+        subject.sendAllSynchronous()
 
         // then
         val expectedNumberOfMoveEvents = config.expectedTimestamps.size
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendUpTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendUpTest.kt
index c826df1..4530955f 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendUpTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/SendUpTest.kt
@@ -20,27 +20,22 @@
 import android.view.MotionEvent.ACTION_POINTER_DOWN
 import android.view.MotionEvent.ACTION_POINTER_UP
 import android.view.MotionEvent.ACTION_UP
-import androidx.test.filters.SmallTest
 import androidx.compose.ui.geometry.Offset
+import androidx.test.filters.SmallTest
 import androidx.ui.test.InputDispatcher.Companion.eventPeriod
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.util.MotionEventRecorder
 import androidx.ui.test.util.assertHasValidEventTimes
 import androidx.ui.test.util.expectError
 import androidx.ui.test.util.verifyEvent
 import androidx.ui.test.util.verifyPointer
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestRule
 
 /**
- * Tests if [AndroidInputDispatcher.sendUp] works
+ * Tests if [AndroidInputDispatcher.enqueueUp] works
  */
 @SmallTest
-class SendUpTest {
+class SendUpTest : InputDispatcherTest() {
     companion object {
         // pointerIds
         private const val pointer1 = 11
@@ -51,22 +46,12 @@
         private val position2_1 = Offset(21f, 21f)
     }
 
-    @get:Rule
-    val inputDispatcherRule: TestRule = InputDispatcherTestRule(disableDispatchInRealTime = true)
-
-    private val recorder = MotionEventRecorder()
-    private val subject = AndroidInputDispatcher(recorder::recordEvent)
-
-    @After
-    fun tearDown() {
-        recorder.disposeEvents()
-    }
-
     @Test
     fun onePointer() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendUpAndCheck(pointer1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateUpAndCheck(pointer1)
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -83,9 +68,10 @@
 
     @Test
     fun onePointerWithDelay() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendUpAndCheck(pointer1, 2 * eventPeriod)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateUpAndCheck(pointer1, 2 * eventPeriod)
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -103,11 +89,12 @@
 
     @Test
     fun multiplePointers_ascending() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
-        subject.sendUpAndCheck(pointer1)
-        subject.sendUpAndCheck(pointer2)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
+        subject.generateUpAndCheck(pointer1)
+        subject.generateUpAndCheck(pointer2)
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -132,11 +119,12 @@
 
     @Test
     fun multiplePointers_descending() {
-        subject.sendDownAndCheck(pointer1, position1_1)
-        subject.sendDownAndCheck(pointer2, position2_1)
-        subject.sendUpAndCheck(pointer2)
-        subject.sendUpAndCheck(pointer1)
+        subject.generateDownAndCheck(pointer1, position1_1)
+        subject.generateDownAndCheck(pointer2, position2_1)
+        subject.generateUpAndCheck(pointer2)
+        subject.generateUpAndCheck(pointer1)
         subject.verifyNoGestureInProgress()
+        subject.sendAllSynchronous()
 
         recorder.assertHasValidEventTimes()
         recorder.events.apply {
@@ -162,33 +150,33 @@
     @Test
     fun upWithoutDown() {
         expectError<IllegalStateException> {
-            subject.sendUp(pointer1)
+            subject.enqueueUp(pointer1)
         }
     }
 
     @Test
     fun upWrongPointerId() {
-        subject.sendDown(pointer1, position1_1)
+        subject.enqueueDown(pointer1, position1_1)
         expectError<IllegalArgumentException> {
-            subject.sendUp(pointer2)
+            subject.enqueueUp(pointer2)
         }
     }
 
     @Test
     fun upAfterUp() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendUp(pointer1)
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueUp(pointer1)
         expectError<IllegalStateException> {
-            subject.sendUp(pointer1)
+            subject.enqueueUp(pointer1)
         }
     }
 
     @Test
     fun upAfterCancel() {
-        subject.sendDown(pointer1, position1_1)
-        subject.sendCancel()
+        subject.enqueueDown(pointer1, position1_1)
+        subject.enqueueCancel()
         expectError<IllegalStateException> {
-            subject.sendUp(pointer1)
+            subject.enqueueUp(pointer1)
         }
     }
 }
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt
index 085e3de..c770634f 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt
@@ -17,15 +17,15 @@
 package androidx.ui.test
 
 import androidx.collection.SparseArrayCompat
-import androidx.compose.ui.node.Owner
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.node.Owner
 import androidx.compose.ui.platform.AndroidOwner
-import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.android.AndroidOwnerRegistry
 import androidx.compose.ui.unit.Duration
 import androidx.compose.ui.unit.inMilliseconds
 import androidx.compose.ui.unit.milliseconds
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.android.AndroidOwnerRegistry
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -37,19 +37,20 @@
  * Interface for dispatching full and partial gestures.
  *
  * Full gestures:
- * * [sendClick]
- * * [sendSwipe]
- * * [sendSwipes]
+ * * [enqueueClick]
+ * * [enqueueSwipe]
+ * * [enqueueSwipes]
  *
  * Partial gestures:
- * * [sendDown]
- * * [sendMove]
- * * [sendUp]
- * * [sendCancel]
+ * * [enqueueDown]
+ * * [enqueueMove]
+ * * [enqueueUp]
+ * * [enqueueCancel]
+ * * [movePointer]
  * * [getCurrentPosition]
  *
  * Chaining methods:
- * * [delay]
+ * * [enqueueDelay]
  */
 internal abstract class AndroidBaseInputDispatcher : InputDispatcher() {
     companion object : AndroidOwnerRegistry.OnRegistrationChangedListener {
@@ -75,7 +76,7 @@
         }
     }
 
-    internal override fun saveState(owner: Owner?) {
+    override fun saveState(owner: Owner?) {
         if (owner != null && AndroidOwnerRegistry.getUnfilteredOwners().contains(owner)) {
             states[owner] = InputDispatcherState(nextDownTime, partialGesture)
         }
@@ -91,16 +92,14 @@
     val isGestureInProgress: Boolean
         get() = partialGesture != null
 
-    /**
-     * The current time, in the time scale used by gesture events.
-     */
-    protected abstract val now: Long
+    abstract override val now: Long
 
     /**
      * Generates the downTime of the next gesture with the given [duration]. The gesture's
      * [duration] is necessary to facilitate chaining of gestures: if another gesture is made
      * after the next one, it will start exactly [duration] after the start of the next gesture.
-     * Always use this method to determine the downTime of the [down event][down] of a gesture.
+     * Always use this method to determine the downTime of the [down event][enqueueDown] of a
+     * gesture.
      *
      * If the duration is unknown when calling this method, use a duration of zero and update
      * with [moveNextDownTime] when the duration is known, or use [moveNextDownTime]
@@ -126,27 +125,28 @@
     }
 
     /**
-     * Increases the eventTime with the gi§ven [time]. Also pushes the downTime for the next
+     * Increases the eventTime with the given [time]. Also pushes the downTime for the next
      * chained gesture by the same amount to facilitate chaining.
      */
-    private fun PartialGesture.increaseEventTime(time: Long = InputDispatcher.eventPeriod) {
+    private fun PartialGesture.increaseEventTime(time: Long = eventPeriod) {
         moveNextDownTime(time.milliseconds)
         lastEventTime += time
     }
 
     /**
-     * Delays the next gesture by the given [duration], but does not block. Guarantees that the
-     * first event time of the next gesture will be exactly [duration] later then if that gesture
-     * would be injected without this delay, provided that the next gesture is started using the
-     * same [InputDispatcher] instance as the one used to end the last gesture.
+     * Adds a delay between the end of the last full or current partial gesture of the given
+     * [duration]. Guarantees that the first event time of the next gesture will be exactly
+     * [duration] later then if that gesture would be injected without this delay, provided that
+     * the next gesture is started using the same [InputDispatcher] instance as the one used to
+     * end the last gesture.
      *
      * Note: this does not affect the time of the next event for the _current_ partial gesture,
-     * using [move], [up] and [cancel], but it will affect the time of the _next_
-     * gesture (including partial gestures started with [down]).
+     * using [enqueueMove], [enqueueUp] and [enqueueCancel], but it will affect the time of the
+     * _next_ gesture (including partial gestures started with [enqueueDown]).
      *
      * @param duration The duration of the delay. Must be positive
      */
-    override fun delay(duration: Duration) {
+    override fun enqueueDelay(duration: Duration) {
         require(duration >= Duration.Zero) {
             "duration of a delay can only be positive, not $duration"
         }
@@ -154,77 +154,69 @@
     }
 
     /**
-     * Blocks until uptime of [time], if [dispatchInRealTime] is `true`.
-     */
-    protected fun sleepUntil(time: Long) {
-        if (InputDispatcher.dispatchInRealTime) {
-            val currTime = now
-            if (currTime < time) {
-                Thread.sleep(time - currTime)
-            }
-        }
-    }
-
-    /**
-     * Sends a click event at [position]. There will be 10ms in between the down and the up
-     * event. This method blocks until all input events have been dispatched.
+     * Generates a click event at [position]. There will be 10ms in between the down and the up
+     * event. The generated events are enqueued in this [InputDispatcher] and will be sent when
+     * [sendAllSynchronous] is called at the end of [performGesture].
      *
      * @param position The coordinate of the click
      */
-    override fun sendClick(position: Offset) {
-        sendDown(0, position)
-        sendMove()
-        sendUp(0)
+    override fun enqueueClick(position: Offset) {
+        enqueueDown(0, position)
+        enqueueMove()
+        enqueueUp(0)
     }
 
     /**
-     * Sends a swipe gesture from [start] to [end] with the given [duration]. This method blocks
-     * until all input events have been dispatched.
+     * Generates a swipe gesture from [start] to [end] with the given [duration]. The generated
+     * events are enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous]
+     * is called at the end of [performGesture].
      *
      * @param start The start position of the gesture
      * @param end The end position of the gesture
      * @param duration The duration of the gesture
      */
-    override fun sendSwipe(start: Offset, end: Offset, duration: Duration) {
+    override fun enqueueSwipe(start: Offset, end: Offset, duration: Duration) {
         val durationFloat = duration.inMilliseconds().toFloat()
-        sendSwipe(
+        enqueueSwipe(
             curve = { lerp(start, end, it / durationFloat) },
             duration = duration
         )
     }
 
     /**
-     * Sends a swipe gesture from [curve]&#40;0) to [curve]&#40;[duration]), following the route
-     * defined by [curve]. Will force sampling of an event at all times defined in [keyTimes].
-     * The number of events sampled between the key times is implementation dependent. This
-     * method blocks until all input events have been dispatched.
+     * Generates a swipe gesture from [curve]&#40;0) to [curve]&#40;[duration]), following the
+     * route defined by [curve]. Will force sampling of an event at all times defined in
+     * [keyTimes]. The number of events sampled between the key times is implementation
+     * dependent. The generated events are enqueued in this [InputDispatcher] and will be sent
+     * when [sendAllSynchronous] is called at the end of [performGesture].
      *
      * @param curve The function that defines the position of the gesture over time
      * @param duration The duration of the gesture
      * @param keyTimes An optional list of timestamps in milliseconds at which a move event must
      * be sampled
      */
-    override fun sendSwipe(
+    override fun enqueueSwipe(
         curve: (Long) -> Offset,
         duration: Duration,
         keyTimes: List<Long>
     ) {
-        sendSwipes(listOf(curve), duration, keyTimes)
+        enqueueSwipes(listOf(curve), duration, keyTimes)
     }
 
     /**
-     * Sends [curves].size simultaneous swipe gestures, each swipe going from
+     * Generates [curves].size simultaneous swipe gestures, each swipe going from
      * [curves]&#91;i&#93;(0) to [curves]&#91;i&#93;([duration]), following the route defined by
      * [curves]&#91;i&#93;. Will force sampling of an event at all times defined in [keyTimes].
-     * The number of events sampled between the key times is implementation dependent. This
-     * method blocks until all input events have been dispatched.
+     * The number of events sampled between the key times is implementation dependent. The
+     * generated events are enqueued in this [InputDispatcher] and will be sent when
+     * [sendAllSynchronous] is called at the end of [performGesture].
      *
      * @param curves The functions that define the position of the gesture over time
      * @param duration The duration of the gestures
      * @param keyTimes An optional list of timestamps in milliseconds at which a move event must
      * be sampled
      */
-    override fun sendSwipes(
+    override fun enqueueSwipes(
         curves: List<(Long) -> Offset>,
         duration: Duration,
         keyTimes: List<Long>
@@ -246,7 +238,7 @@
 
         // Send down events
         curves.forEachIndexed { i, curve ->
-            sendDown(i, curve(startTime))
+            enqueueDown(i, curve(startTime))
         }
 
         // Send move events between each consecutive pair in [t0, ..keyTimes, tN]
@@ -265,16 +257,16 @@
 
         // And end with up events
         repeat(curves.size) {
-            sendUp(it)
+            enqueueUp(it)
         }
     }
 
     /**
-     * Sends move events between `f([t0])` and `f([tN])` during the time window `(downTime + t0,
-     * downTime + tN]`, using [fs] to sample the coordinate of each event. The number of events
-     * sent (#numEvents) is such that the time between each event is as close to [eventPeriod] as
-     * possible, but at least 1. The first event is sent at time `downTime + (tN - t0) /
-     * #numEvents`, the last event is sent at time tN.
+     * Generates move events between `f([t0])` and `f([tN])` during the time window `(downTime +
+     * t0, downTime + tN]`, using [fs] to sample the coordinate of each event. The number of
+     * events sent (#numEvents) is such that the time between each event is as close to
+     * [InputDispatcher.eventPeriod] as possible, but at least 1. The first event is sent at time
+     * `downTime + (tN - t0) / #numEvents`, the last event is sent at time tN.
      *
      * @param fs The functions that define the coordinates of the respective gestures over time
      * @param t0 The start time of this segment of the swipe, in milliseconds relative to downTime
@@ -297,7 +289,7 @@
             fs.forEachIndexed { i, f ->
                 movePointer(i, f(t))
             }
-            sendMove(t - tPrev)
+            enqueueMove(t - tPrev)
             tPrev = t
         }
     }
@@ -315,26 +307,28 @@
     }
 
     /**
-     * Sends a down event at [position] for the pointer with the given [pointerId], starting a
-     * new partial gesture. A partial gesture can only be started if none was currently ongoing
-     * for that pointer. Pointer ids may be reused during the same gesture. This method blocks
-     * until the input event has been dispatched.
+     * Generates a down event at [position] for the pointer with the given [pointerId], starting
+     * a new partial gesture. A partial gesture can only be started if none was currently ongoing
+     * for that pointer. Pointer ids may be reused during the same gesture. The generated event
+     * is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is called
+     * at the end of [performGesture].
      *
-     * It is possible to mix partial gestures with full gestures (e.g. send a [click][click]
-     * during a partial gesture), as long as you make sure that the default pointer id (id=0) is
-     * free to be used by the full gesture.
+     * It is possible to mix partial gestures with full gestures (e.g. generate a [click]
+     * [enqueueClick] during a partial gesture), as long as you make sure that the default
+     * pointer id (id=0) is free to be used by the full gesture.
      *
      * A full gesture starts with a down event at some position (with this method) that indicates
-     * a finger has started touching the screen, followed by zero or more [down][down],
-     * [move][move] and [up][up] events that respectively indicate that another finger
-     * started touching the screen, a finger moved around or a finger was lifted up from the
-     * screen. A gesture is finished when [up][up] lifts the last remaining finger from the
-     * screen, or when a single [cancel][cancel] event is sent.
+     * a finger has started touching the screen, followed by zero or more [down][enqueueDown],
+     * [move][enqueueMove] and [up][enqueueUp] events that respectively indicate that another
+     * finger started touching the screen, a finger moved around or a finger was lifted up from
+     * the screen. A gesture is finished when [up][enqueueUp] lifts the last remaining finger
+     * from the screen, or when a single [cancel][enqueueCancel] event is generated.
      *
      * Partial gestures don't have to be defined all in the same [performGesture] block, but
      * keep in mind that while the gesture is not complete, all code you execute in between
      * blocks that progress the gesture, will be executed while imaginary fingers are actively
-     * touching the screen.
+     * touching the screen. All events generated during a single [performGesture] block are sent
+     * together at the end of that block.
      *
      * In the context of testing, it is not necessary to complete a gesture with an up or cancel
      * event, if the test ends before it expects the finger to be lifted from the screen.
@@ -343,11 +337,11 @@
      * @param position The coordinate of the down event
      *
      * @see movePointer
-     * @see move
-     * @see up
-     * @see cancel
+     * @see enqueueMove
+     * @see enqueueUp
+     * @see enqueueCancel
      */
-    override fun sendDown(pointerId: Int, position: Offset) {
+    override fun enqueueDown(pointerId: Int, position: Offset) {
         var gesture = partialGesture
 
         // Check if this pointer is not already down
@@ -366,25 +360,25 @@
         }
 
         // Send the DOWN event
-        gesture.sendDown(pointerId)
+        gesture.enqueueDown(pointerId)
     }
 
     /**
      * Updates the position of the pointer with the given [pointerId] to the given [position],
-     * but does not send a move event. Use this to move multiple pointers simultaneously. To send
-     * the next move event, which will contain the current position of _all_ pointers (not just
-     * the moved ones), call [move] without arguments. If you move one or more pointers and
-     * then call [down] or [up], without calling [move] first, a move event will be
-     * sent right before that down or up event. See [down] for more information on how to make
-     * complete gestures from partial gestures.
+     * but does not generate a move event. Use this to move multiple pointers simultaneously. To
+     * generate the next move event, which will contain the current position of _all_ pointers
+     * (not just the moved ones), call [enqueueMove] without arguments. If you move one or more
+     * pointers and then call [enqueueDown] or [enqueueUp], without calling [enqueueMove] first,
+     * a move event will be generated right before that down or up event. See [enqueueDown] for
+     * more information on how to make complete gestures from partial gestures.
      *
-     * @param pointerId The id of the pointer to move, as supplied in [down]
+     * @param pointerId The id of the pointer to move, as supplied in [enqueueDown]
      * @param position The position to move the pointer to
      *
-     * @see down
-     * @see move
-     * @see up
-     * @see cancel
+     * @see enqueueDown
+     * @see enqueueMove
+     * @see enqueueUp
+     * @see enqueueCancel
      */
     override fun movePointer(pointerId: Int, position: Offset) {
         val gesture = partialGesture
@@ -402,16 +396,18 @@
     }
 
     /**
-     * Sends a move event [delay] milliseconds after the previous injected event of this gesture,
-     * without moving any of the pointers. The default [delay] is [10][eventPeriod] milliseconds.
-     * Use this to commit all changes in pointer location made with [movePointer]. The sent event
-     * will contain the current position of all pointers. See [down] for more information on
-     * how to make complete gestures from partial gestures.
+     * Generates a move event [delay] milliseconds after the previous injected event of this
+     * gesture, without moving any of the pointers. The default [delay] is [10 milliseconds]
+     * [InputDispatcher.eventPeriod]. Use this to commit all changes in pointer location made
+     * with [movePointer]. The generated event will contain the current position of all pointers.
+     * It is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is
+     * called at the end of [performGesture]. See [enqueueDown] for more information on how to
+     * make complete gestures from partial gestures.
      *
      * @param delay The time in milliseconds between the previously injected event and the move
-     * event. [10][eventPeriod] milliseconds by default.
+     * event. [10 milliseconds][InputDispatcher.eventPeriod] by default.
      */
-    override fun sendMove(delay: Long) {
+    override fun enqueueMove(delay: Long) {
         val gesture = checkNotNull(partialGesture) {
             "Cannot send MOVE event, no gesture is in progress"
         }
@@ -420,26 +416,27 @@
         }
 
         gesture.increaseEventTime(delay)
-        gesture.sendMove()
+        gesture.enqueueMove()
         gesture.hasPointerUpdates = false
     }
 
     /**
-     * Sends an up event for the given [pointerId] at the current position of that pointer,
+     * Generates an up event for the given [pointerId] at the current position of that pointer,
      * [delay] milliseconds after the previous injected event of this gesture. The default
-     * [delay] is 0 milliseconds. This method blocks until the input event has been dispatched.
-     * See [down] for more information on how to make complete gestures from partial gestures.
+     * [delay] is 0 milliseconds. The generated event is enqueued in this [InputDispatcher] and
+     * will be sent when [sendAllSynchronous] is called at the end of [performGesture]. See
+     * [enqueueDown] for more information on how to make complete gestures from partial gestures.
      *
-     * @param pointerId The id of the pointer to lift up, as supplied in [down]
+     * @param pointerId The id of the pointer to lift up, as supplied in [enqueueDown]
      * @param delay The time in milliseconds between the previously injected event and the move
      * event. 0 milliseconds by default.
      *
-     * @see down
+     * @see enqueueDown
      * @see movePointer
-     * @see move
-     * @see cancel
+     * @see enqueueMove
+     * @see enqueueCancel
      */
-    override fun sendUp(pointerId: Int, delay: Long) {
+    override fun enqueueUp(pointerId: Int, delay: Long) {
         val gesture = partialGesture
 
         // Check if this pointer is in the gesture
@@ -457,7 +454,7 @@
         gesture.increaseEventTime(delay)
 
         // First send the UP event
-        gesture.sendUp(pointerId)
+        gesture.enqueueUp(pointerId)
 
         // Then remove the pointer, and end the gesture if no pointers are left
         gesture.lastPositions.remove(pointerId)
@@ -467,20 +464,21 @@
     }
 
     /**
-     * Sends a cancel event [delay] milliseconds after the previous injected event of this
-     * gesture. The default [delay] is [10][eventPeriod] milliseconds. This method blocks until
-     * the input event has been dispatched. See [down] for more information on how to make
-     * complete gestures from partial gestures.
+     * Generates a cancel event [delay] milliseconds after the previous injected event of this
+     * gesture. The default [delay] is [10 milliseconds][InputDispatcher.eventPeriod]. The
+     * generated event is enqueued in this [InputDispatcher] and will be sent when
+     * [sendAllSynchronous] is called at the end of [performGesture]. See [enqueueDown] for more
+     * information on how to make complete gestures from partial gestures.
      *
      * @param delay The time in milliseconds between the previously injected event and the cancel
-     * event. [10][eventPeriod] milliseconds by default.
+     * event. [10 milliseconds][InputDispatcher.eventPeriod] by default.
      *
-     * @see down
+     * @see enqueueDown
      * @see movePointer
-     * @see move
-     * @see up
+     * @see enqueueMove
+     * @see enqueueUp
      */
-    override fun sendCancel(delay: Long) {
+    override fun enqueueCancel(delay: Long) {
         val gesture = checkNotNull(partialGesture) {
             "Cannot send CANCEL event, no gesture is in progress"
         }
@@ -489,37 +487,37 @@
         }
 
         gesture.increaseEventTime(delay)
-        gesture.sendCancel()
+        gesture.enqueueCancel()
         partialGesture = null
     }
 
     /**
-     * Sends a MOVE event with all pointer locations, if any of the pointers has been moved by
+     * Generates a MOVE event with all pointer locations, if any of the pointers has been moved by
      * [movePointer] since the last MOVE event.
      */
     private fun PartialGesture.flushPointerUpdates() {
         if (hasPointerUpdates) {
-            sendMove(eventPeriod)
+            enqueueMove(eventPeriod)
         }
     }
 
-    protected abstract fun PartialGesture.sendDown(pointerId: Int)
+    protected abstract fun PartialGesture.enqueueDown(pointerId: Int)
 
-    protected abstract fun PartialGesture.sendMove()
+    protected abstract fun PartialGesture.enqueueMove()
 
-    protected abstract fun PartialGesture.sendUp(pointerId: Int)
+    protected abstract fun PartialGesture.enqueueUp(pointerId: Int)
 
-    protected abstract fun PartialGesture.sendCancel()
+    protected abstract fun PartialGesture.enqueueCancel()
 
     /**
-     * A test rule that modifies [InputDispatcher]s behavior. Can be used to disable
-     * dispatching of MotionEvents in real time (skips the sleep before injection of an event) or
-     * to change the time between consecutive injected events.
+     * A test rule that modifies [InputDispatcher]s behavior. Can be used to disable dispatching
+     * of MotionEvents in real time (skips the suspend before injection of an event) or to change
+     * the time between consecutive injected events.
      *
      * @param disableDispatchInRealTime If set, controls whether or not events with an eventTime
      * in the future will be dispatched as soon as possible or at that exact eventTime. If
-     * `false` or not set, will sleep until the eventTime, if `true`, will send the event
-     * immediately without blocking. See also [dispatchInRealTime].
+     * `false` or not set, will suspend until the eventTime, if `true`, will send the event
+     * immediately without suspending. See also [InputDispatcher.dispatchInRealTime].
      * @param eventPeriodOverride If set, specifies a different period in milliseconds between
      * two consecutive injected motion events injected by this [InputDispatcher]. If not
      * set, the event period of 10 milliseconds is unchanged.
@@ -538,10 +536,10 @@
         inner class ModifyingStatement(private val base: Statement) : Statement() {
             override fun evaluate() {
                 if (disableDispatchInRealTime) {
-                    InputDispatcher.dispatchInRealTime = false
+                    dispatchInRealTime = false
                 }
                 if (eventPeriodOverride != null) {
-                    InputDispatcher.eventPeriod = eventPeriodOverride
+                    eventPeriod = eventPeriodOverride
                 }
                 try {
                     base.evaluate()
@@ -550,7 +548,7 @@
                         dispatchInRealTime = true
                     }
                     if (eventPeriodOverride != null) {
-                        InputDispatcher.eventPeriod = 10L
+                        eventPeriod = 10L
                     }
                 }
             }
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt
index 36713b1..edfc28b 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt
@@ -16,62 +16,65 @@
 
 package androidx.ui.test.android
 
-import android.os.Handler
-import android.os.Looper
 import android.os.SystemClock
-import android.util.Log
 import android.view.MotionEvent
 import android.view.MotionEvent.ACTION_CANCEL
 import android.view.MotionEvent.ACTION_DOWN
 import android.view.MotionEvent.ACTION_MOVE
 import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT
 import android.view.MotionEvent.ACTION_POINTER_UP
 import android.view.MotionEvent.ACTION_UP
+import androidx.compose.runtime.dispatch.AndroidUiDispatcher
 import androidx.compose.ui.geometry.Offset
 import androidx.ui.test.AndroidBaseInputDispatcher
+import androidx.ui.test.InputDispatcher
 import androidx.ui.test.PartialGesture
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
 
 internal class AndroidInputDispatcher(
     private val sendEvent: (MotionEvent) -> Unit
 ) : AndroidBaseInputDispatcher() {
 
-    private val handler = Handler(Looper.getMainLooper())
+    private val batchLock = Any()
+    private var batchedEvents = mutableListOf<MotionEvent>()
+    private var acceptEvents = true
 
     override val now: Long get() = SystemClock.uptimeMillis()
 
-    override fun PartialGesture.sendDown(pointerId: Int) {
-        sendMotionEvent(
+    override fun PartialGesture.enqueueDown(pointerId: Int) {
+        batchMotionEvent(
             if (lastPositions.size() == 1) ACTION_DOWN else ACTION_POINTER_DOWN,
             lastPositions.indexOfKey(pointerId)
         )
     }
 
-    override fun PartialGesture.sendMove() {
-        sendMotionEvent(ACTION_MOVE, 0)
+    override fun PartialGesture.enqueueMove() {
+        batchMotionEvent(ACTION_MOVE, 0)
     }
 
-    override fun PartialGesture.sendUp(pointerId: Int) {
-        sendMotionEvent(
+    override fun PartialGesture.enqueueUp(pointerId: Int) {
+        batchMotionEvent(
             if (lastPositions.size() == 1) ACTION_UP else ACTION_POINTER_UP,
             lastPositions.indexOfKey(pointerId)
         )
     }
 
-    override fun PartialGesture.sendCancel() {
-        sendMotionEvent(ACTION_CANCEL, 0)
+    override fun PartialGesture.enqueueCancel() {
+        batchMotionEvent(ACTION_CANCEL, 0)
     }
 
     /**
-     * Sends a MotionEvent with the given [action] and [actionIndex], adding all pointers that
-     * are currently in the gesture.
+     * Generates a MotionEvent with the given [action] and [actionIndex], adding all pointers that
+     * are currently in the gesture, and adds the MotionEvent to the batch.
      *
      * @see MotionEvent.getAction
      * @see MotionEvent.getActionIndex
      */
-    private fun PartialGesture.sendMotionEvent(action: Int, actionIndex: Int) {
-        sendMotionEvent(
+    private fun PartialGesture.batchMotionEvent(action: Int, actionIndex: Int) {
+        batchMotionEvent(
             downTime,
             lastEventTime,
             action,
@@ -82,9 +85,9 @@
     }
 
     /**
-     * Sends an event with the given parameters.
+     * Generates an event with the given parameters.
      */
-    private fun sendMotionEvent(
+    private fun batchMotionEvent(
         downTime: Long,
         eventTime: Long,
         action: Int,
@@ -92,52 +95,108 @@
         coordinates: List<Offset>,
         pointerIds: List<Int>
     ) {
-        sleepUntil(eventTime)
-        sendAndRecycleEvent(
-            MotionEvent.obtain(
-                /* downTime = */ downTime,
-                /* eventTime = */ eventTime,
-                /* action = */ action + (actionIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
-                /* pointerCount = */ coordinates.size,
-                /* pointerProperties = */ Array(coordinates.size) {
-                    MotionEvent.PointerProperties().apply { id = pointerIds[it] }
-                },
-                /* pointerCoords = */ Array(coordinates.size) {
-                    MotionEvent.PointerCoords().apply {
-                        x = coordinates[it].x
-                        y = coordinates[it].y
-                    }
-                },
-                /* metaState = */ 0,
-                /* buttonState = */ 0,
-                /* xPrecision = */ 0f,
-                /* yPrecision = */ 0f,
-                /* deviceId = */ 0,
-                /* edgeFlags = */ 0,
-                /* source = */ 0,
-                /* flags = */ 0
+        synchronized(batchLock) {
+            check(acceptEvents) {
+                "Can't enqueue event (" +
+                        "downTime=$downTime, " +
+                        "eventTime=$eventTime, " +
+                        "action=$action, " +
+                        "actionIndex=$actionIndex, " +
+                        "pointerIds=$pointerIds, " +
+                        "coordinates=$coordinates" +
+                        "), events have already been (or are being) dispatched or disposed"
+            }
+            batchedEvents.add(
+                MotionEvent.obtain(
+                    /* downTime = */ downTime,
+                    /* eventTime = */ eventTime,
+                    /* action = */ action + (actionIndex shl ACTION_POINTER_INDEX_SHIFT),
+                    /* pointerCount = */ coordinates.size,
+                    /* pointerProperties = */ Array(coordinates.size) {
+                        MotionEvent.PointerProperties().apply { id = pointerIds[it] }
+                    },
+                    /* pointerCoords = */ Array(coordinates.size) {
+                        MotionEvent.PointerCoords().apply {
+                            x = coordinates[it].x
+                            y = coordinates[it].y
+                        }
+                    },
+                    /* metaState = */ 0,
+                    /* buttonState = */ 0,
+                    /* xPrecision = */ 0f,
+                    /* yPrecision = */ 0f,
+                    /* deviceId = */ 0,
+                    /* edgeFlags = */ 0,
+                    /* source = */ 0,
+                    /* flags = */ 0
+                )
             )
-        )
+        }
+    }
+
+    override fun sendAllSynchronous() {
+        runBlocking {
+            withContext(AndroidUiDispatcher.Main) {
+                checkAndStopAcceptingEvents()
+                val copy = batchedEvents.toList()
+                batchedEvents.clear()
+                val iterator = copy.iterator()
+                try {
+                    while (iterator.hasNext()) {
+                        val event = iterator.next()
+                        sendAndRecycleEvent(event)
+                    }
+                } finally {
+                    // In case we were cancelled, or an exception was thrown,
+                    // stop injecting and recycle all left over events
+                    while (iterator.hasNext()) {
+                        try {
+                            iterator.next().recycle()
+                        } catch (ignore: Throwable) {
+                            // ignore all errors, just continue recycling
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    override fun dispose() {
+        if (stopAcceptingEvents()) {
+            batchedEvents.forEach { it.recycle() }
+            batchedEvents.clear()
+        }
+    }
+
+    private fun checkAndStopAcceptingEvents() {
+        synchronized(batchLock) {
+            check(acceptEvents) { "Events have already been (or are being) dispatched or disposed" }
+            acceptEvents = false
+        }
+    }
+
+    private fun stopAcceptingEvents(): Boolean {
+        synchronized(batchLock) {
+            return acceptEvents.also { acceptEvents = false }
+        }
     }
 
     /**
-     * Sends the [event] to the MotionEvent dispatcher and [recycles][MotionEvent.recycle] it
-     * regardless of the result. This method blocks until the event is sent.
+     * Sends and recycles the given [event]. If [InputDispatcher.dispatchInRealTime] is `true`,
+     * suspends until [now] is equal to the event's `eventTime`. Doesn't suspend otherwise, or if
+     * the event's `eventTime` is before [now].
      */
-    private fun sendAndRecycleEvent(event: MotionEvent) {
-        val latch = CountDownLatch(1)
-        handler.post {
-            try {
-                sendEvent(event)
-            } finally {
-                event.recycle()
-                latch.countDown()
+    private suspend fun sendAndRecycleEvent(event: MotionEvent) {
+        try {
+            if (dispatchInRealTime) {
+                val delayMs = event.eventTime - now
+                if (delayMs > 0) {
+                    delay(delayMs)
+                }
             }
-        }
-        if (!latch.await(5, TimeUnit.SECONDS)) {
-            Log.w("AndroidInputDispatcher", "Dispatching of MotionEvent $event took longer than " +
-                    "5 seconds to complete. This should typically only take a few milliseconds.")
-            latch.await()
+            sendEvent(event)
+        } finally {
+            event.recycle()
         }
     }
 }
diff --git a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/Actions.kt b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/Actions.kt
index 444f7ed..e17b0b3 100644
--- a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/Actions.kt
+++ b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/Actions.kt
@@ -82,6 +82,11 @@
  * responsibility of the caller to make sure partial gestures don't leave the test in an
  * inconsistent state.
  *
+ * All events that are injected from the [block] are batched together and sent after [block] is
+ * complete. This method blocks until all those events have been injected, which normally takes
+ * as long as the duration of the gesture. If an error occurs during execution of [block] or
+ * injection of the events, all (subsequent) events are dropped and the error is thrown here.
+ *
  * This method must not be called from the main thread. The block will be executed on the same
  * thread as the caller.
  *
@@ -107,7 +112,11 @@
         try {
             block()
         } finally {
-            dispose()
+            try {
+                inputDispatcher.sendAllSynchronous()
+            } finally {
+                dispose()
+            }
         }
     }
     return this
diff --git a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/GestureScope.kt b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/GestureScope.kt
index 9d00613..6dd6e79 100644
--- a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/GestureScope.kt
+++ b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/GestureScope.kt
@@ -65,6 +65,9 @@
  * one finger might tap the screen while another is making a gesture. In that case, make sure the
  * partial gesture uses a non-default pointer id.
  *
+ * Note that all events generated by the gesture methods are batched together and sent as a whole
+ * after [performGesture] has executed its code block.
+ *
  * Next to the functions, [GestureScope] also exposes several properties that allow you to get
  * [coordinates][Offset] within a node, like the [top left corner][topLeft], its [center], or
  * 20% to the left of the right edge and 10% below the top edge ([percentOffset]).
@@ -116,6 +119,7 @@
 
     internal fun dispose() {
         inputDispatcher.saveState(owner)
+        inputDispatcher.dispose()
         _semanticsNode = null
         _inputDispatcher = null
     }
@@ -294,7 +298,7 @@
  * omitted, the center position will be used.
  */
 fun GestureScope.click(position: Offset = center) {
-    inputDispatcher.sendClick(localToGlobal(position))
+    inputDispatcher.enqueueClick(localToGlobal(position))
 }
 
 /**
@@ -336,9 +340,9 @@
         "Time between clicks in double click can be at most ${DoubleTapTimeout - 10.milliseconds}ms"
     }
     val globalPosition = localToGlobal(position)
-    inputDispatcher.sendClick(globalPosition)
-    inputDispatcher.delay(delay)
-    inputDispatcher.sendClick(globalPosition)
+    inputDispatcher.enqueueClick(globalPosition)
+    inputDispatcher.enqueueDelay(delay)
+    inputDispatcher.enqueueClick(globalPosition)
 }
 
 /**
@@ -358,7 +362,7 @@
 ) {
     val globalStart = localToGlobal(start)
     val globalEnd = localToGlobal(end)
-    inputDispatcher.sendSwipe(globalStart, globalEnd, duration)
+    inputDispatcher.enqueueSwipe(globalStart, globalEnd, duration)
 }
 
 /**
@@ -387,7 +391,7 @@
     val globalEnd1 = localToGlobal(end1)
     val durationFloat = duration.inMilliseconds().toFloat()
 
-    inputDispatcher.sendSwipes(
+    inputDispatcher.enqueueSwipes(
         listOf<(Long) -> Offset>(
             { lerp(globalStart0, globalEnd0, it / durationFloat) },
             { lerp(globalStart1, globalEnd1, it / durationFloat) }
@@ -453,7 +457,7 @@
     val fx = createFunctionForVelocity(durationMs, globalStart.x, globalEnd.x, vx)
     val fy = createFunctionForVelocity(durationMs, globalStart.y, globalEnd.y, vy)
 
-    inputDispatcher.sendSwipe({ t -> Offset(fx(t), fy(t)) }, duration)
+    inputDispatcher.enqueueSwipe({ t -> Offset(fx(t), fy(t)) }, duration)
 }
 
 /**
@@ -607,7 +611,7 @@
  */
 fun GestureScope.down(pointerId: Int, position: Offset) {
     val globalPosition = localToGlobal(position)
-    inputDispatcher.sendDown(pointerId, globalPosition)
+    inputDispatcher.enqueueDown(pointerId, globalPosition)
 }
 
 /**
@@ -723,7 +727,7 @@
  * [movePointerBy].
  */
 fun GestureScope.move() {
-    inputDispatcher.sendMove()
+    inputDispatcher.enqueueMove()
 }
 
 /**
@@ -735,7 +739,7 @@
  * @param pointerId The id of the pointer to lift up, as supplied in [down]
  */
 fun GestureScope.up(pointerId: Int = 0) {
-    inputDispatcher.sendUp(pointerId)
+    inputDispatcher.enqueueUp(pointerId)
 }
 
 /**
@@ -743,5 +747,5 @@
  * current position of all active pointers.
  */
 fun GestureScope.cancel() {
-    inputDispatcher.sendCancel()
+    inputDispatcher.enqueueCancel()
 }
diff --git a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt
index f5d5070..cc9e94a 100644
--- a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt
+++ b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt
@@ -25,9 +25,9 @@
 internal abstract class InputDispatcher {
     companion object {
         /**
-         * Whether or not events with an eventTime in the future should be dispatched at that
-         * exact eventTime. If `true`, will sleep until the eventTime, if `false`, will send the
-         * event immediately without blocking.
+         * Whether or not injection of events should be suspended in between events until [now]
+         * is at least the `eventTime` of the next event to inject. If `true`, will suspend until
+         * the next `eventTime`, if `false`, will send the event immediately without suspending.
          */
         internal var dispatchInRealTime: Boolean = true
 
@@ -41,42 +41,59 @@
             internal set
     }
 
+    /**
+     * The current time, in the time scale used by gesture events.
+     */
+    protected abstract val now: Long
+
+    /**
+     * Sends all enqueued events and blocks while they are dispatched. Will suspend before
+     * dispatching an event until [now] is at least that event's timestamp. If an exception is
+     * thrown during the process, all events that haven't yet been dispatched will be dropped.
+     */
+    internal abstract fun sendAllSynchronous()
+
     internal abstract fun saveState(owner: Owner?)
 
-    abstract fun sendClick(position: Offset)
+    /**
+     * Called when this [InputDispatcher] is about to be discarded, from [GestureScope.dispose].
+     */
+    internal abstract fun dispose()
 
-    abstract fun sendSwipe(start: Offset, end: Offset, duration: Duration)
+    abstract fun enqueueClick(position: Offset)
 
-    abstract fun sendSwipe(
+    abstract fun enqueueSwipe(start: Offset, end: Offset, duration: Duration)
+
+    abstract fun enqueueSwipe(
         curve: (Long) -> Offset,
         duration: Duration,
         keyTimes: List<Long> = emptyList()
     )
 
-    abstract fun sendSwipes(
+    abstract fun enqueueSwipes(
         curves: List<(Long) -> Offset>,
         duration: Duration,
         keyTimes: List<Long> = emptyList()
     )
 
-    abstract fun delay(duration: Duration)
+    abstract fun enqueueDelay(duration: Duration)
 
-    abstract fun sendDown(pointerId: Int, position: Offset)
+    abstract fun enqueueDown(pointerId: Int, position: Offset)
 
-    abstract fun sendUp(pointerId: Int, delay: Long = 0)
+    abstract fun enqueueUp(pointerId: Int, delay: Long = 0)
 
-    abstract fun sendCancel(delay: Long = eventPeriod)
+    abstract fun enqueueCancel(delay: Long = eventPeriod)
 
     abstract fun movePointer(pointerId: Int, position: Offset)
 
-    abstract fun sendMove(delay: Long = eventPeriod)
+    abstract fun enqueueMove(delay: Long = eventPeriod)
 
     abstract fun getCurrentPosition(pointerId: Int): Offset?
 }
 
 /**
- * The state of an [InputDispatcher], saved when the [BaseGestureScope] is disposed and restored
- * when the [BaseGestureScope] is recreated.
+ * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored when
+ * the [GestureScope] is recreated.
  *
  * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
  * This property will only be restored if an incomplete gesture was in progress when the state of
diff --git a/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index 8459fd4..32b1e38 100644
--- a/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -530,9 +530,9 @@
                 val cursorXOffset = col * fontSizeInPx
 
                 val expectRect = Rect(
-                    left = cursorXOffset - cursorWidth / 2,
+                    left = cursorXOffset,
                     top = top,
-                    right = cursorXOffset + cursorWidth / 2,
+                    right = cursorXOffset,
                     bottom = top + fontSizeInPx
                 )
                 val actualRect = paragraph.getCursorRect(i)
@@ -548,9 +548,9 @@
             val cursorXOffset = col * fontSizeInPx
 
             val expectRect = Rect(
-                left = cursorXOffset - cursorWidth / 2,
+                left = cursorXOffset,
                 top = top,
-                right = cursorXOffset + cursorWidth / 2,
+                right = cursorXOffset,
                 bottom = top + fontSizeInPx
             )
             val actualRect = paragraph.getCursorRect(text.length)
diff --git a/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index 6c61b7e..e8fbb6b 100644
--- a/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/ui/ui-text-core/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -65,8 +65,6 @@
 
     private val resourceLoader = TestFontResourceLoader(context)
 
-    private val cursorWidth = 4f
-
     @Test
     fun empty_string() {
         with(defaultDensity) {
@@ -588,9 +586,9 @@
                 val cursorXOffset = i * fontSizeInPx
                 assertThat(cursorRect).isEqualTo(
                     Rect(
-                        left = cursorXOffset - cursorWidth / 2,
+                        left = cursorXOffset,
                         top = 0f,
-                        right = cursorXOffset + cursorWidth / 2,
+                        right = cursorXOffset,
                         bottom = fontSizeInPx
                     )
                 )
@@ -615,9 +613,9 @@
                 val cursorXOffset = i * fontSizeInPx
                 assertThat(paragraph.getCursorRect(i)).isEqualTo(
                     Rect(
-                        left = cursorXOffset - cursorWidth / 2,
+                        left = cursorXOffset,
                         top = 0f,
-                        right = cursorXOffset + cursorWidth / 2,
+                        right = cursorXOffset,
                         bottom = fontSizeInPx
                     )
                 )
@@ -627,9 +625,9 @@
                 val cursorXOffset = (i % charsPerLine) * fontSizeInPx
                 assertThat(paragraph.getCursorRect(i)).isEqualTo(
                     Rect(
-                        left = cursorXOffset - cursorWidth / 2,
+                        left = cursorXOffset,
                         top = fontSizeInPx,
-                        right = cursorXOffset + cursorWidth / 2,
+                        right = cursorXOffset,
                         bottom = fontSizeInPx * 2.2f
                     )
                 )
@@ -651,9 +649,9 @@
             // Cursor before '\n'
             assertThat(paragraph.getCursorRect(3)).isEqualTo(
                 Rect(
-                    left = 3 * fontSizeInPx - cursorWidth / 2,
+                    left = 3 * fontSizeInPx,
                     top = 0f,
-                    right = 3 * fontSizeInPx + cursorWidth / 2,
+                    right = 3 * fontSizeInPx,
                     bottom = fontSizeInPx
                 )
             )
@@ -661,9 +659,9 @@
             // Cursor after '\n'
             assertThat(paragraph.getCursorRect(4)).isEqualTo(
                 Rect(
-                    left = -cursorWidth / 2,
+                    left = 0f,
                     top = fontSizeInPx,
-                    right = cursorWidth / 2,
+                    right = 0f,
                     bottom = fontSizeInPx * 2.2f
                 )
             )
@@ -684,9 +682,9 @@
             // Cursor before '\n'
             assertThat(paragraph.getCursorRect(3)).isEqualTo(
                 Rect(
-                    left = 3 * fontSizeInPx - cursorWidth / 2,
+                    left = 3 * fontSizeInPx,
                     top = 0f,
-                    right = 3 * fontSizeInPx + cursorWidth / 2,
+                    right = 3 * fontSizeInPx,
                     bottom = fontSizeInPx
                 )
             )
@@ -694,9 +692,9 @@
             // Cursor after '\n'
             assertThat(paragraph.getCursorRect(4)).isEqualTo(
                 Rect(
-                    left = -cursorWidth / 2,
+                    left = 0f,
                     top = fontSizeInPx,
-                    right = cursorWidth / 2,
+                    right = 0f,
                     bottom = fontSizeInPx * 2.2f
                 )
             )
@@ -719,9 +717,9 @@
                 val cursorXOffset = (text.length - i) * fontSizeInPx
                 assertThat(paragraph.getCursorRect(i)).isEqualTo(
                     Rect(
-                        left = cursorXOffset - cursorWidth / 2,
+                        left = cursorXOffset,
                         top = 0f,
-                        right = cursorXOffset + cursorWidth / 2,
+                        right = cursorXOffset,
                         bottom = fontSizeInPx
                     )
                 )
@@ -746,9 +744,9 @@
                 val cursorXOffset = (charsPerLine - i) * fontSizeInPx
                 assertThat(paragraph.getCursorRect(i)).isEqualTo(
                     Rect(
-                        left = cursorXOffset - cursorWidth / 2,
+                        left = cursorXOffset,
                         top = 0f,
-                        right = cursorXOffset + cursorWidth / 2,
+                        right = cursorXOffset,
                         bottom = fontSizeInPx
                     )
                 )
@@ -758,9 +756,9 @@
                 val cursorXOffset = (charsPerLine - i % charsPerLine) * fontSizeInPx
                 assertThat(paragraph.getCursorRect(i)).isEqualTo(
                     Rect(
-                        left = cursorXOffset - cursorWidth / 2,
+                        left = cursorXOffset,
                         top = fontSizeInPx,
-                        right = cursorXOffset + cursorWidth / 2,
+                        right = cursorXOffset,
                         bottom = fontSizeInPx * 2.2f
                     )
                 )
@@ -783,9 +781,9 @@
             // Cursor before '\n'
             assertThat(paragraph.getCursorRect(3)).isEqualTo(
                 Rect(
-                    left = 0 - cursorWidth / 2,
+                    left = 0f,
                     top = 0f,
-                    right = 0 + cursorWidth / 2,
+                    right = 0f,
                     bottom = fontSizeInPx
                 )
             )
@@ -793,9 +791,9 @@
             // Cursor after '\n'
             assertThat(paragraph.getCursorRect(4)).isEqualTo(
                 Rect(
-                    left = 3 * fontSizeInPx - cursorWidth / 2,
+                    left = 3 * fontSizeInPx,
                     top = fontSizeInPx,
-                    right = 3 * fontSizeInPx + cursorWidth / 2,
+                    right = 3 * fontSizeInPx,
                     bottom = fontSizeInPx * 2.2f
                 )
             )
@@ -818,9 +816,9 @@
             // Cursor before '\n'
             assertThat(paragraph.getCursorRect(3)).isEqualTo(
                 Rect(
-                    left = 0 - cursorWidth / 2,
+                    left = 0f,
                     top = 0f,
-                    right = 0 + cursorWidth / 2,
+                    right = 0f,
                     bottom = fontSizeInPx
                 )
             )
@@ -828,9 +826,9 @@
             // Cursor after '\n'
             assertThat(paragraph.getCursorRect(4)).isEqualTo(
                 Rect(
-                    left = -cursorWidth / 2,
+                    left = 0f,
                     top = fontSizeInPx,
-                    right = +cursorWidth / 2,
+                    right = 0f,
                     bottom = fontSizeInPx * 2.2f
                 )
             )
diff --git a/ui/ui-text-core/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.kt b/ui/ui-text-core/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.kt
index 8665c44..abff882 100644
--- a/ui/ui-text-core/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.kt
+++ b/ui/ui-text-core/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.kt
@@ -256,14 +256,15 @@
         if (offset !in 0..charSequence.length) {
             throw AssertionError("offset($offset) is out of bounds (0,${charSequence.length}")
         }
-        val cursorWidth = 4.0f
         val horizontal = layout.getPrimaryHorizontal(offset)
         val line = layout.getLineForOffset(offset)
 
+        // The width of the cursor is not taken into account. The callers of this API should use
+        // rect.left to get the start X position and then adjust it according to the width if needed
         return Rect(
-            horizontal - 0.5f * cursorWidth,
+            horizontal,
             layout.getLineTop(line),
-            horizontal + 0.5f * cursorWidth,
+            horizontal,
             layout.getLineBottom(line)
         )
     }
diff --git a/ui/ui-text/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/ui/ui-text/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 821ff0a..112dd17 100644
--- a/ui/ui-text/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/ui/ui-text/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -194,23 +194,14 @@
         manager.textToolbar = TextToolbarAmbient.current
         manager.hapticFeedBack = HapticFeedBackAmbient.current
 
-        val focusObserver = Modifier.focusObserver { focusState ->
-
-            // No Change in focus state, just make sure the keyboard is shown/hidden.
-            if (state.hasFocus == focusState.isFocused) {
-                textInputService?.run {
-                    if (focusState.isFocused) {
-                        showSoftwareKeyboard(state.inputSession)
-                    } else {
-                        hideSoftwareKeyboard(state.inputSession)
-                    }
-                }
+        val focusObserver = Modifier.focusObserver {
+            if (state.hasFocus == it.isFocused) {
                 return@focusObserver
             }
 
-            state.hasFocus = focusState.isFocused
+            state.hasFocus = it.isFocused
 
-            if (focusState.isFocused) {
+            if (it.isFocused) {
                 state.inputSession = TextFieldDelegate.onFocus(
                     textInputService,
                     value,
@@ -238,7 +229,7 @@
                                 coords,
                                 textInputService,
                                 state.inputSession,
-                                focusState.isFocused,
+                                state.hasFocus,
                                 offsetMap
                             )
                         }
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt
index bd10702..b1a399e 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/preview/ComposeViewAdapter.kt
@@ -228,7 +228,7 @@
     @VisibleForTesting
     internal fun findAndSubscribeTransitions() {
         val slotTrees = slotTableRecord.store.map { it.asTree() }
-        slotTrees.map { tree -> tree.firstOrNull { it.name == composableName } }
+        slotTrees.mapNotNull { tree -> tree.firstOrNull { it.name == composableName } }
             .firstOrNull()?.let { composable ->
                 // Find all the AnimationClockObservers corresponding to transition animations
                 val observers = composable.findAll {