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](0) to [curve]([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](0) to [curve]([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][i](0) to [curves][i]([duration]), following the route defined by
* [curves][i]. 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 {