Merge "Add AggregateAppFunctionInvoker" into androidx-main
diff --git a/appfunctions/appfunctions-common/api/current.txt b/appfunctions/appfunctions-common/api/current.txt
index 16cb63b..ba35715 100644
--- a/appfunctions/appfunctions-common/api/current.txt
+++ b/appfunctions/appfunctions-common/api/current.txt
@@ -1,11 +1,14 @@
// Signature format: 4.0
package androidx.appfunctions {
- public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionException {
+ public abstract class AppFunctionAppException extends androidx.appfunctions.AppFunctionException {
+ }
+
+ public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionAppException {
ctor public AppFunctionAppUnknownException(optional String? errorMessage);
}
- public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionSystemException {
ctor public AppFunctionCancelledException(optional String? errorMessage);
}
@@ -18,93 +21,69 @@
property public abstract android.content.Context context;
}
- public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionDeniedException(optional String? errorMessage);
}
- public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionDisabledException(optional String? errorMessage);
}
- public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionElementAlreadyExistsException(optional String? errorMessage);
}
- public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionElementNotFoundException(optional String? errorMessage);
}
- public class AppFunctionException extends java.lang.Exception {
+ public abstract class AppFunctionException extends java.lang.Exception {
ctor public AppFunctionException(int errorCode, optional String? errorMessage);
- method public final int getErrorCategory();
- method public final int getErrorCode();
method public final String? getErrorMessage();
- property public final int errorCategory;
- property public final int errorCode;
property public final String? errorMessage;
field public static final androidx.appfunctions.AppFunctionException.Companion Companion;
- field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8
- field public static final int ERROR_CANCELLED = 2001; // 0x7d1
- field public static final int ERROR_CATEGORY_APP = 3; // 0x3
- field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
- field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
- field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
- field public static final int ERROR_DENIED = 1000; // 0x3e8
- field public static final int ERROR_DISABLED = 1002; // 0x3ea
- field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb
- field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9
- field public static final int ERROR_LIMIT_EXCEEDED = 1501; // 0x5dd
- field public static final int ERROR_NOT_SUPPORTED = 3501; // 0xdad
- field public static final int ERROR_PERMISSION_REQUIRED = 3500; // 0xdac
- field public static final int ERROR_RESOURCE_ALREADY_EXISTS = 1502; // 0x5de
- field public static final int ERROR_RESOURCE_NOT_FOUND = 1500; // 0x5dc
- field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0
}
public static final class AppFunctionException.Companion {
- property public static final int ERROR_APP_UNKNOWN_ERROR;
- property public static final int ERROR_CANCELLED;
- property public static final int ERROR_CATEGORY_APP;
- property public static final int ERROR_CATEGORY_REQUEST_ERROR;
- property public static final int ERROR_CATEGORY_SYSTEM;
- property public static final int ERROR_CATEGORY_UNKNOWN;
- property public static final int ERROR_DENIED;
- property public static final int ERROR_DISABLED;
- property public static final int ERROR_FUNCTION_NOT_FOUND;
- property public static final int ERROR_INVALID_ARGUMENT;
- property public static final int ERROR_LIMIT_EXCEEDED;
- property public static final int ERROR_NOT_SUPPORTED;
- property public static final int ERROR_PERMISSION_REQUIRED;
- property public static final int ERROR_RESOURCE_ALREADY_EXISTS;
- property public static final int ERROR_RESOURCE_NOT_FOUND;
- property public static final int ERROR_SYSTEM_ERROR;
}
- public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionFunctionNotFoundException(optional String? errorMessage);
}
- public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionInvalidArgumentException(optional String? errorMessage);
}
- public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionLimitExceededException(optional String? errorMessage);
}
- public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionAppException {
ctor public AppFunctionNotSupportedException(optional String? errorMessage);
}
- public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionAppException {
ctor public AppFunctionPermissionRequiredException(optional String? errorMessage);
}
+ public abstract class AppFunctionRequestException extends androidx.appfunctions.AppFunctionException {
+ }
+
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface AppFunctionSerializable {
}
- public final class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
- ctor public AppFunctionSystemException(optional String? errorMessage);
+ public abstract class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
+ }
+
+ public final class AppFunctionSystemUnknownException extends androidx.appfunctions.AppFunctionSystemException {
+ ctor public AppFunctionSystemUnknownException(optional String? errorMessage);
+ }
+
+ public final class AppFunctionUnknownException extends androidx.appfunctions.AppFunctionException {
+ ctor public AppFunctionUnknownException(int unknownErrorCode, optional String? errorMessage);
+ method public int getUnknownErrorCode();
+ property public final int unknownErrorCode;
}
}
diff --git a/appfunctions/appfunctions-common/api/restricted_current.txt b/appfunctions/appfunctions-common/api/restricted_current.txt
index 16cb63b..ba35715 100644
--- a/appfunctions/appfunctions-common/api/restricted_current.txt
+++ b/appfunctions/appfunctions-common/api/restricted_current.txt
@@ -1,11 +1,14 @@
// Signature format: 4.0
package androidx.appfunctions {
- public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionException {
+ public abstract class AppFunctionAppException extends androidx.appfunctions.AppFunctionException {
+ }
+
+ public final class AppFunctionAppUnknownException extends androidx.appfunctions.AppFunctionAppException {
ctor public AppFunctionAppUnknownException(optional String? errorMessage);
}
- public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionCancelledException extends androidx.appfunctions.AppFunctionSystemException {
ctor public AppFunctionCancelledException(optional String? errorMessage);
}
@@ -18,93 +21,69 @@
property public abstract android.content.Context context;
}
- public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionDeniedException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionDeniedException(optional String? errorMessage);
}
- public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionDisabledException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionDisabledException(optional String? errorMessage);
}
- public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionElementAlreadyExistsException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionElementAlreadyExistsException(optional String? errorMessage);
}
- public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionElementNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionElementNotFoundException(optional String? errorMessage);
}
- public class AppFunctionException extends java.lang.Exception {
+ public abstract class AppFunctionException extends java.lang.Exception {
ctor public AppFunctionException(int errorCode, optional String? errorMessage);
- method public final int getErrorCategory();
- method public final int getErrorCode();
method public final String? getErrorMessage();
- property public final int errorCategory;
- property public final int errorCode;
property public final String? errorMessage;
field public static final androidx.appfunctions.AppFunctionException.Companion Companion;
- field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8
- field public static final int ERROR_CANCELLED = 2001; // 0x7d1
- field public static final int ERROR_CATEGORY_APP = 3; // 0x3
- field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
- field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
- field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
- field public static final int ERROR_DENIED = 1000; // 0x3e8
- field public static final int ERROR_DISABLED = 1002; // 0x3ea
- field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb
- field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9
- field public static final int ERROR_LIMIT_EXCEEDED = 1501; // 0x5dd
- field public static final int ERROR_NOT_SUPPORTED = 3501; // 0xdad
- field public static final int ERROR_PERMISSION_REQUIRED = 3500; // 0xdac
- field public static final int ERROR_RESOURCE_ALREADY_EXISTS = 1502; // 0x5de
- field public static final int ERROR_RESOURCE_NOT_FOUND = 1500; // 0x5dc
- field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0
}
public static final class AppFunctionException.Companion {
- property public static final int ERROR_APP_UNKNOWN_ERROR;
- property public static final int ERROR_CANCELLED;
- property public static final int ERROR_CATEGORY_APP;
- property public static final int ERROR_CATEGORY_REQUEST_ERROR;
- property public static final int ERROR_CATEGORY_SYSTEM;
- property public static final int ERROR_CATEGORY_UNKNOWN;
- property public static final int ERROR_DENIED;
- property public static final int ERROR_DISABLED;
- property public static final int ERROR_FUNCTION_NOT_FOUND;
- property public static final int ERROR_INVALID_ARGUMENT;
- property public static final int ERROR_LIMIT_EXCEEDED;
- property public static final int ERROR_NOT_SUPPORTED;
- property public static final int ERROR_PERMISSION_REQUIRED;
- property public static final int ERROR_RESOURCE_ALREADY_EXISTS;
- property public static final int ERROR_RESOURCE_NOT_FOUND;
- property public static final int ERROR_SYSTEM_ERROR;
}
- public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionFunctionNotFoundException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionFunctionNotFoundException(optional String? errorMessage);
}
- public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionInvalidArgumentException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionInvalidArgumentException(optional String? errorMessage);
}
- public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionLimitExceededException extends androidx.appfunctions.AppFunctionRequestException {
ctor public AppFunctionLimitExceededException(optional String? errorMessage);
}
- public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionNotSupportedException extends androidx.appfunctions.AppFunctionAppException {
ctor public AppFunctionNotSupportedException(optional String? errorMessage);
}
- public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionException {
+ public final class AppFunctionPermissionRequiredException extends androidx.appfunctions.AppFunctionAppException {
ctor public AppFunctionPermissionRequiredException(optional String? errorMessage);
}
+ public abstract class AppFunctionRequestException extends androidx.appfunctions.AppFunctionException {
+ }
+
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface AppFunctionSerializable {
}
- public final class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
- ctor public AppFunctionSystemException(optional String? errorMessage);
+ public abstract class AppFunctionSystemException extends androidx.appfunctions.AppFunctionException {
+ }
+
+ public final class AppFunctionSystemUnknownException extends androidx.appfunctions.AppFunctionSystemException {
+ ctor public AppFunctionSystemUnknownException(optional String? errorMessage);
+ }
+
+ public final class AppFunctionUnknownException extends androidx.appfunctions.AppFunctionException {
+ ctor public AppFunctionUnknownException(int unknownErrorCode, optional String? errorMessage);
+ method public int getUnknownErrorCode();
+ property public final int unknownErrorCode;
}
}
diff --git a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt
index 31108ed..a8cbef9 100644
--- a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt
+++ b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionExceptionTest.kt
@@ -23,85 +23,10 @@
class AppFunctionExceptionTest {
@Test
- fun testConstructor_withoutMessageAndExtras() {
- val exception = AppFunctionException(AppFunctionException.ERROR_DENIED)
-
- assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
- assertThat(exception.errorMessage).isNull()
- assertThat(exception.extras).isEqualTo(Bundle.EMPTY)
- }
-
- @Test
- fun testConstructor_withoutExtras() {
- val exception = AppFunctionException(AppFunctionException.ERROR_DENIED, "testMessage")
-
- assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
- assertThat(exception.errorMessage).isEqualTo("testMessage")
- assertThat(exception.extras).isEqualTo(Bundle.EMPTY)
- }
-
- @Test
- fun testConstructor() {
- val extras = Bundle().apply { putString("testKey", "testValue") }
- val exception =
- AppFunctionException(AppFunctionException.ERROR_DENIED, "testMessage", extras)
-
- assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
- assertThat(exception.errorMessage).isEqualTo("testMessage")
- assertThat(exception.extras.getString("testKey")).isEqualTo("testValue")
- }
-
- @Test
- fun testErrorCategory_RequestError() {
- assertThat(AppFunctionException(AppFunctionException.ERROR_DENIED).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- assertThat(AppFunctionException(AppFunctionException.ERROR_INVALID_ARGUMENT).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- assertThat(AppFunctionException(AppFunctionException.ERROR_DISABLED).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- assertThat(
- AppFunctionException(AppFunctionException.ERROR_FUNCTION_NOT_FOUND).errorCategory
- )
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- assertThat(
- AppFunctionException(AppFunctionException.ERROR_RESOURCE_NOT_FOUND).errorCategory
- )
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- assertThat(AppFunctionException(AppFunctionException.ERROR_LIMIT_EXCEEDED).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- assertThat(
- AppFunctionException(AppFunctionException.ERROR_RESOURCE_ALREADY_EXISTS)
- .errorCategory
- )
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_REQUEST_ERROR)
- }
-
- @Test
- fun testErrorCategory_SystemError() {
- assertThat(AppFunctionException(AppFunctionException.ERROR_SYSTEM_ERROR).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_SYSTEM)
- assertThat(AppFunctionException(AppFunctionException.ERROR_CANCELLED).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_SYSTEM)
- }
-
- @Test
- fun testErrorCategory_AppError() {
- assertThat(AppFunctionException(AppFunctionException.ERROR_APP_UNKNOWN_ERROR).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_APP)
- assertThat(
- AppFunctionException(AppFunctionException.ERROR_PERMISSION_REQUIRED).errorCategory
- )
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_APP)
- assertThat(AppFunctionException(AppFunctionException.ERROR_NOT_SUPPORTED).errorCategory)
- .isEqualTo(AppFunctionException.ERROR_CATEGORY_APP)
- }
-
- @Test
fun testTransformToPlatformExtensionsClass() {
assumeAppFunctionExtensionLibraryAvailable()
val extras = Bundle().apply { putString("testKey", "testValue") }
- val exception =
- AppFunctionException(AppFunctionException.ERROR_DENIED, "testMessage", extras)
+ val exception = AppFunctionDeniedException("testMessage", extras)
val platformException = exception.toPlatformExtensionsClass()
@@ -111,20 +36,81 @@
}
@Test
- fun testCreateFromPlatformExtensionsClass() {
+ fun testCreateFromPlatformExtensionsClass_knownClasses() {
+ assumeAppFunctionExtensionLibraryAvailable()
+
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_APP_UNKNOWN_ERROR,
+ AppFunctionAppUnknownException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_PERMISSION_REQUIRED,
+ AppFunctionPermissionRequiredException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_NOT_SUPPORTED,
+ AppFunctionNotSupportedException::class.java
+ )
+
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_DENIED,
+ AppFunctionDeniedException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_INVALID_ARGUMENT,
+ AppFunctionInvalidArgumentException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_DISABLED,
+ AppFunctionDisabledException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_FUNCTION_NOT_FOUND,
+ AppFunctionFunctionNotFoundException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_LIMIT_EXCEEDED,
+ AppFunctionLimitExceededException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_RESOURCE_ALREADY_EXISTS,
+ AppFunctionElementAlreadyExistsException::class.java
+ )
+
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_SYSTEM_ERROR,
+ AppFunctionSystemUnknownException::class.java
+ )
+ testCreateFromPlatformExtensionsClass(
+ AppFunctionException.ERROR_CANCELLED,
+ AppFunctionCancelledException::class.java
+ )
+ }
+
+ @Test
+ fun testCreateFromPlatformExtensionsClass_unknownErrorCode() {
+ assumeAppFunctionExtensionLibraryAvailable()
+
+ testCreateFromPlatformExtensionsClass(123456, AppFunctionUnknownException::class.java)
+ }
+
+ private fun <E : AppFunctionException> testCreateFromPlatformExtensionsClass(
+ errorCode: Int,
+ exceptionClass: Class<E>
+ ) {
assumeAppFunctionExtensionLibraryAvailable()
val extras = Bundle().apply { putString("testKey", "testValue") }
val platformException =
com.android.extensions.appfunctions.AppFunctionException(
- AppFunctionException.ERROR_DENIED,
+ errorCode,
"testMessage",
extras
)
val exception = AppFunctionException.fromPlatformExtensionsClass(platformException)
- assertThat(exception).isInstanceOf(AppFunctionDeniedException::class.java)
- assertThat(exception.errorCode).isEqualTo(AppFunctionException.ERROR_DENIED)
+ assertThat(exception).isInstanceOf(exceptionClass)
+ assertThat(exception.errorCode).isEqualTo(errorCode)
assertThat(exception.errorMessage).isEqualTo("testMessage")
assertThat(exception.extras.getString("testKey")).isEqualTo("testValue")
}
diff --git a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt
index d59f337..89ccbf0 100644
--- a/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt
+++ b/appfunctions/appfunctions-common/src/androidTest/java/androidx/appfunctions/AppFunctionSystemExceptionsTest.kt
@@ -22,9 +22,9 @@
class AppFunctionSystemExceptionsTest {
@Test
fun testErrorCategory_SystemError() {
- assertThat(AppFunctionSystemException().errorCode)
+ assertThat(AppFunctionSystemUnknownException().errorCode)
.isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR)
- assertThat(AppFunctionSystemException().errorCategory)
+ assertThat(AppFunctionSystemUnknownException().errorCategory)
.isEqualTo(AppFunctionException.ERROR_CATEGORY_SYSTEM)
assertThat(AppFunctionCancelledException().errorCode)
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
index 2e73ca0..441c6bb 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
@@ -17,19 +17,25 @@
package androidx.appfunctions
import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_APP
+
+/**
+ * Thrown when an error is caused by the app providing the function.
+ *
+ * <p>For example, the app crashed when the system is executing the request.
+ */
+public abstract class AppFunctionAppException
+internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
+ AppFunctionException(errorCode, errorMessage, extras)
/**
* Thrown when an unknown error occurred while processing the call in the AppFunctionService.
*
* <p>This error is thrown when the service is connected in the remote application but an unexpected
* error is thrown from the bound application.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
*/
public class AppFunctionAppUnknownException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_APP_UNKNOWN_ERROR, errorMessage, extras) {
+ AppFunctionAppException(ERROR_APP_UNKNOWN_ERROR, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
@@ -43,12 +49,10 @@
*
* <p> This is different from [AppFunctionDeniedException] in that the required permission is
* missing from the target app, as opposed to the caller.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
*/
public class AppFunctionPermissionRequiredException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_PERMISSION_REQUIRED, errorMessage, extras) {
+ AppFunctionAppException(ERROR_PERMISSION_REQUIRED, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
@@ -58,12 +62,10 @@
*
* <p>For example, a clock app might support updating timer properties such as label but may not
* allow updating the timer's duration once the timer has already started.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
*/
public class AppFunctionNotSupportedException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_NOT_SUPPORTED, errorMessage, extras) {
+ AppFunctionAppException(ERROR_NOT_SUPPORTED, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt
index 65b79ee..e273dd4 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionException.kt
@@ -25,10 +25,10 @@
*
* This exception can be used by the app to report errors to the caller.
*/
-public open class AppFunctionException
+public abstract class AppFunctionException
internal constructor(
/** The error code. */
- @ErrorCode public val errorCode: Int,
+ @ErrorCode internal val errorCode: Int,
/** The error message. */
public val errorMessage: String?,
internal val extras: Bundle
@@ -65,7 +65,7 @@
* error category.
*/
@ErrorCategory
- public val errorCategory: Int =
+ internal val errorCategory: Int =
when (errorCode) {
in 1000..1999 -> ERROR_CATEGORY_REQUEST_ERROR
in 2000..2999 -> ERROR_CATEGORY_SYSTEM
@@ -126,26 +126,28 @@
exception.errorMessage,
exception.extras
)
+ ERROR_SYSTEM_ERROR ->
+ AppFunctionSystemUnknownException(exception.errorMessage, exception.extras)
ERROR_CANCELLED ->
AppFunctionCancelledException(exception.errorMessage, exception.extras)
- ERROR_APP_UNKNOWN_ERROR, ->
- AppFunctionCancelledException(exception.errorMessage, exception.extras)
+ ERROR_APP_UNKNOWN_ERROR ->
+ AppFunctionAppUnknownException(exception.errorMessage, exception.extras)
ERROR_PERMISSION_REQUIRED ->
AppFunctionPermissionRequiredException(exception.errorMessage, exception.extras)
ERROR_NOT_SUPPORTED ->
AppFunctionNotSupportedException(exception.errorMessage, exception.extras)
else ->
- AppFunctionException(
+ AppFunctionUnknownException(
exception.errorCode,
exception.errorMessage,
- exception.extras
+ exception.extras,
)
}
}
// Error categories
/** The error category is unknown. */
- public const val ERROR_CATEGORY_UNKNOWN: Int = 0
+ internal const val ERROR_CATEGORY_UNKNOWN: Int = 0
/**
* The error is caused by the app requesting a function execution.
@@ -155,7 +157,7 @@
*
* <p>Errors in the category fall in the range 1000-1999 inclusive.
*/
- public const val ERROR_CATEGORY_REQUEST_ERROR: Int = 1
+ internal const val ERROR_CATEGORY_REQUEST_ERROR: Int = 1
/**
* The error is caused by an issue in the system.
@@ -164,7 +166,7 @@
*
* <p>Errors in the category fall in the range 2000-2999 inclusive.
*/
- public const val ERROR_CATEGORY_SYSTEM: Int = 2
+ internal const val ERROR_CATEGORY_SYSTEM: Int = 2
/**
* The error is caused by the app providing the function.
@@ -173,7 +175,7 @@
*
* <p>Errors in the category fall in the range 3000-3999 inclusive.
*/
- public const val ERROR_CATEGORY_APP: Int = 3
+ internal const val ERROR_CATEGORY_APP: Int = 3
// Error codes
/**
@@ -184,7 +186,7 @@
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_DENIED: Int = 1000
+ internal const val ERROR_DENIED: Int = 1000
/**
* The caller supplied invalid arguments to the execution request.
@@ -193,21 +195,21 @@
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_INVALID_ARGUMENT: Int = 1001
+ internal const val ERROR_INVALID_ARGUMENT: Int = 1001
/**
* The caller tried to execute a disabled app function.
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_DISABLED: Int = 1002
+ internal const val ERROR_DISABLED: Int = 1002
/**
* The caller tried to execute a function that does not exist.
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_FUNCTION_NOT_FOUND: Int = 1003
+ internal const val ERROR_FUNCTION_NOT_FOUND: Int = 1003
// SDK-defined error codes in the [ERROR_CATEGORY_REQUEST_ERROR] category start from 1500.
/**
@@ -215,14 +217,14 @@
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_RESOURCE_NOT_FOUND: Int = 1500
+ internal const val ERROR_RESOURCE_NOT_FOUND: Int = 1500
/**
* The caller exceeded the allowed request rate.
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_LIMIT_EXCEEDED: Int = 1501
+ internal const val ERROR_LIMIT_EXCEEDED: Int = 1501
/**
* The caller tried to create a resource/entity that already exists or has conflicts with
@@ -230,14 +232,14 @@
*
* <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
- public const val ERROR_RESOURCE_ALREADY_EXISTS: Int = 1502
+ internal const val ERROR_RESOURCE_ALREADY_EXISTS: Int = 1502
/**
* An internal unexpected error coming from the system.
*
* <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
*/
- public const val ERROR_SYSTEM_ERROR: Int = 2000
+ internal const val ERROR_SYSTEM_ERROR: Int = 2000
/**
* The operation was cancelled. Use this error code to report that a cancellation is done
@@ -245,7 +247,7 @@
*
* <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
*/
- public const val ERROR_CANCELLED: Int = 2001
+ internal const val ERROR_CANCELLED: Int = 2001
/**
* An unknown error occurred while processing the call in the AppFunctionService.
@@ -255,7 +257,7 @@
*
* <p>This error is in the [ERROR_CATEGORY_APP] category.
*/
- public const val ERROR_APP_UNKNOWN_ERROR: Int = 3000
+ internal const val ERROR_APP_UNKNOWN_ERROR: Int = 3000
// SDK-defined error codes in the [ERROR_CATEGORY_APP] category start from 3500.
/**
@@ -271,7 +273,7 @@
*
* <p>This error is in the [ERROR_CATEGORY_APP] category.
*/
- public const val ERROR_PERMISSION_REQUIRED: Int = 3500
+ internal const val ERROR_PERMISSION_REQUIRED: Int = 3500
/**
* Indicates the action is not supported by the app.
@@ -282,6 +284,25 @@
*
* <p>This error is in the [ERROR_CATEGORY_APP] category.
*/
- public const val ERROR_NOT_SUPPORTED: Int = 3501
+ internal const val ERROR_NOT_SUPPORTED: Int = 3501
}
}
+
+/**
+ * Thrown when an unknown error has occurred.
+ *
+ * <p> This Exception is used when the error doesn't belong to any other AppFunctionException. Note
+ * that this different from [AppFunctionAppUnknownException], in that the error wasn't necessarily
+ * caused by the app.
+ */
+public class AppFunctionUnknownException
+internal constructor(
+ public val unknownErrorCode: Int,
+ errorMessage: String? = null,
+ extras: Bundle
+) : AppFunctionException(unknownErrorCode, errorMessage, extras) {
+ public constructor(
+ unknownErrorCode: Int,
+ errorMessage: String? = null
+ ) : this(unknownErrorCode, errorMessage, Bundle.EMPTY)
+}
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
index 2a4377b..fc16714 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
@@ -17,19 +17,26 @@
package androidx.appfunctions
import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_REQUEST_ERROR
+
+/**
+ * Thrown when the error is caused by the app requesting a function execution.
+ *
+ * <p>For example, the caller provided invalid parameters in the execution request e.g. an invalid
+ * function ID.
+ */
+public abstract class AppFunctionRequestException
+internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
+ AppFunctionException(errorCode, errorMessage, extras)
/**
* Thrown when the caller does not have the permission to execute an app function.
*
* <p> This is different from [AppFunctionPermissionRequiredException] in that the caller is missing
* this specific permission, as opposed to the target app missing a permission.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
public class AppFunctionDeniedException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_DENIED, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_DENIED, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
@@ -38,13 +45,11 @@
* Thrown when the caller supplied invalid arguments to ExecuteAppFunctionRequest's parameters.
*
* <p>This error may be considered similar to [IllegalArgumentException].
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
// TODO(b/389738031): add reference to ExecuteAppFunctionRequest's builder when it is added.
public class AppFunctionInvalidArgumentException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_INVALID_ARGUMENT, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_INVALID_ARGUMENT, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
@@ -53,49 +58,35 @@
* Thrown when the caller tried to execute a disabled app function. An app function can be enabled
* at runtime through the AppFunctionManager or by setting enabledByDefault=true in the AppFunction
* annotation.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
// TODO(b/389738031): add reference to setAppFunctionEnabled and @AppFunction when they are added.
public class AppFunctionDisabledException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_DISABLED, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_DISABLED, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
-/**
- * Thrown when the caller tries to execute a function that does not exist.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller tries to execute a function that does not exist. */
public class AppFunctionFunctionNotFoundException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_FUNCTION_NOT_FOUND, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_FUNCTION_NOT_FOUND, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
-/**
- * Thrown when the caller tried to request a resource/entity that does not exist.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller tried to request a resource/entity that does not exist. */
public class AppFunctionElementNotFoundException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_RESOURCE_NOT_FOUND, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_RESOURCE_NOT_FOUND, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
-/**
- * Thrown when the caller exceeded the allowed request rate.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller exceeded the allowed request rate. */
public class AppFunctionLimitExceededException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_LIMIT_EXCEEDED, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_LIMIT_EXCEEDED, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
@@ -103,12 +94,10 @@
/**
* Thrown when the caller tried to create a resource/entity that already exists or has conflicts
* with existing resource/entity.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
*/
public class AppFunctionElementAlreadyExistsException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_RESOURCE_ALREADY_EXISTS, errorMessage, extras) {
+ AppFunctionRequestException(ERROR_RESOURCE_ALREADY_EXISTS, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
index fbe8bcf..66596b4 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
@@ -17,30 +17,32 @@
package androidx.appfunctions
import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_SYSTEM
/**
* Thrown when an internal unexpected error comes from the system.
*
* <p>For example, the AppFunctionService implementation is not found by the system.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
*/
-public class AppFunctionSystemException
+public abstract class AppFunctionSystemException
+internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
+ AppFunctionException(errorCode, errorMessage, extras)
+
+/**
+ * Thrown when an internal unexpected error comes from the system.
+ *
+ * <p>For example, the AppFunctionService implementation is not found by the system.
+ */
+public class AppFunctionSystemUnknownException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_SYSTEM_ERROR, errorMessage, extras) {
+ AppFunctionSystemException(ERROR_SYSTEM_ERROR, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
-/**
- * Thrown when an operation was cancelled.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
- */
+/** Thrown when an operation was cancelled. */
public class AppFunctionCancelledException
internal constructor(errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(ERROR_CANCELLED, errorMessage, extras) {
+ AppFunctionSystemException(ERROR_CANCELLED, errorMessage, extras) {
public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt
index 0a28700..1fcd4f7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManager.kt
@@ -25,6 +25,7 @@
import androidx.camera.camera2.pipe.core.PruningProcessingQueue
import androidx.camera.camera2.pipe.core.PruningProcessingQueue.Companion.processIn
import androidx.camera.camera2.pipe.core.Threads
+import androidx.camera.camera2.pipe.core.Token
import androidx.camera.camera2.pipe.core.WakeLock
import androidx.camera.camera2.pipe.graph.GraphListener
import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
@@ -138,8 +139,14 @@
}
}
- suspend fun connectTo(virtualCameraState: VirtualCameraState) {
- val token = wakelock.acquire()
+ // Acquire this ActiveCamera. This ensures the camera stay opened for as long as the token is
+ // held. This is important for camera open scenarios where the device manager should acquire
+ // the ActiveCamera for the duration under which it's processing an open request.
+ fun acquire() = wakelock.acquire()
+
+ // TODO: b/389758537, b/390530866 - Make Token non-nullable. If we cannot acquire a token, the
+ // ActiveCamera has issued a RequestClose for this ActiveCamera already.
+ suspend fun connectTo(virtualCameraState: VirtualCameraState, token: Token?) {
val previous = current
current = virtualCameraState
@@ -179,8 +186,18 @@
private val queue =
PruningProcessingQueue<CameraRequest>(prune = ::prune) { process(it) }.processIn(scope)
private val activeCameras: MutableSet<ActiveCamera> = mutableSetOf()
- private val pendingRequestOpens = mutableListOf<RequestOpen>()
- private val pendingRequestOpenActiveCameraMap = mutableMapOf<RequestOpen, ActiveCamera>()
+
+ // PendingRequestOpen stores the context information for the pending RequestOpens to be
+ // connected in concurrent camera scenarios. It contains the request itself, the active camera
+ // it should be connected with, and the usage token for the active camera. The token should
+ // always be closed if the context is removed without being connected to a VirtualCamera.
+ private class PendingRequestOpen(
+ val request: RequestOpen,
+ val activeCamera: ActiveCamera,
+ val token: Token,
+ )
+
+ private val pendingRequestOpens = mutableListOf<PendingRequestOpen>()
override fun open(
cameraId: CameraId,
@@ -358,12 +375,9 @@
if (camerasToClose.isNotEmpty()) {
// Shutdown of cameras should always happen first (and suspend until complete)
activeCameras.removeAll(camerasToClose)
- for (requestOpen in pendingRequestOpens) {
- if (camerasToClose.contains(pendingRequestOpenActiveCameraMap[requestOpen])) {
- pendingRequestOpens.remove(requestOpen)
- pendingRequestOpenActiveCameraMap.remove(requestOpen)
- }
- }
+ disconnectPendingRequestOpens(
+ pendingRequestOpens.filter { camerasToClose.contains(it.activeCamera) }
+ )
for (camera in camerasToClose) {
camera.close()
}
@@ -374,51 +388,37 @@
// Step 2: Open the camera if not opened already.
camera2ErrorProcessor.setActiveVirtualCamera(cameraIdToOpen, request.virtualCamera)
- var realCamera = activeCameras.firstOrNull { it.cameraId == cameraIdToOpen }
- if (realCamera == null) {
- val openResult =
- openCameraWithRetry(
- cameraIdToOpen,
- request.sharedCameraIds,
- request.isForegroundObserver,
- scope,
- )
- when (openResult) {
- is OpenVirtualCameraResult.Success -> {
- realCamera = openResult.activeCamera
- activeCameras.add(realCamera)
- }
- is OpenVirtualCameraResult.Error -> {
- request.virtualCamera.disconnect(openResult.lastCameraError)
- return
- }
- }
+ val result = retrieveActiveCamera(cameraIdToOpen, request)
+ if (result == null) {
+ Log.error { "Failed to retrieve active camera for $cameraIdToOpen" }
+ return
}
+ val realCamera = result.activeCamera
+ val realCameraToken = result.token
// Step 3: Connect the opened camera(s).
if (request.sharedCameraIds.isNotEmpty()) {
- // Both sharedCameraIds and activeCameras are small collections. Looping over them
- // in what equates to nested for-loops are actually going to be more efficient than
- // say, replacing activeCameras with a hashmap.
+ // Both sharedCameraIds and pendingRequestOpenContexts are small collections. Looping
+ // over them in what equates to nested for-loops are actually going to be more efficient
+ // than say, replacing activeCameras with a hashmap.
if (
request.sharedCameraIds.all { cameraId ->
- activeCameras.any { it.cameraId == cameraId }
+ pendingRequestOpens.any { it.activeCamera.cameraId == cameraId }
}
) {
// If the camera of the request and the cameras it is shared with have been
// opened, we can connect the ActiveCameras.
check(!request.isPrewarm)
- realCamera.connectTo(request.virtualCamera)
- connectPendingRequestOpens(request.sharedCameraIds)
+ realCamera.connectTo(request.virtualCamera, realCameraToken)
+ connectPendingRequestOpens(request.sharedCameraIds.toSet())
} else {
// Else, save the request in the pending request queue, and connect the request
// once other cameras are opened.
- pendingRequestOpens.add(request)
- pendingRequestOpenActiveCameraMap[request] = realCamera
+ pendingRequestOpens.add(PendingRequestOpen(request, realCamera, realCameraToken))
}
} else {
if (!request.isPrewarm) {
- realCamera.connectTo(request.virtualCamera)
+ realCamera.connectTo(request.virtualCamera, realCameraToken)
}
}
}
@@ -430,16 +430,13 @@
if (activeCameras.contains(request.activeCamera)) {
activeCameras.remove(request.activeCamera)
}
- for (requestOpen in pendingRequestOpens) {
- // Edge case: There is a possibility that we receive RequestClose after a RequestOpen
- // for concurrent cameras has been processed. As such, we don't want to close the
- // ActiveCamera newly created by the RequestOpen, but only the one RequestClose is
- // aiming to close.
- if (pendingRequestOpenActiveCameraMap[requestOpen] == request.activeCamera) {
- pendingRequestOpens.remove(requestOpen)
- pendingRequestOpenActiveCameraMap.remove(requestOpen)
- }
- }
+
+ // Edge case: There is a possibility that we receive RequestClose after a RequestOpen for
+ // concurrent cameras has been processed. As such, we don't want to close the ActiveCamera
+ // newly created by the RequestOpen, but only the one RequestClose is aiming to close.
+ disconnectPendingRequestOpens(
+ pendingRequestOpens.filter { it.activeCamera == request.activeCamera }
+ )
request.activeCamera.close()
request.activeCamera.awaitClosed()
}
@@ -448,12 +445,9 @@
val cameraId = request.activeCameraId
Log.info { "PruningCamera2DeviceManager#processRequestCloseById(${request.activeCameraId}" }
- for (requestOpen in pendingRequestOpens) {
- if (requestOpen.virtualCamera.cameraId == cameraId) {
- pendingRequestOpens.remove(requestOpen)
- pendingRequestOpenActiveCameraMap.remove(requestOpen)
- }
- }
+ disconnectPendingRequestOpens(
+ pendingRequestOpens.filter { it.request.virtualCamera.cameraId == cameraId }
+ )
val activeCamera = activeCameras.firstOrNull { it.cameraId == cameraId }
if (activeCamera != null) {
activeCameras.remove(activeCamera)
@@ -466,8 +460,7 @@
private suspend fun processRequestCloseAll(requestCloseAll: RequestCloseAll) {
Log.info { "PruningCamera2DeviceManager#processRequestCloseAll()" }
- pendingRequestOpens.clear()
- pendingRequestOpenActiveCameraMap.clear()
+ disconnectPendingRequestOpens(pendingRequestOpens)
for (activeCamera in activeCameras) {
activeCamera.close()
}
@@ -478,6 +471,59 @@
requestCloseAll.deferred.complete(Unit)
}
+ private suspend fun retrieveActiveCamera(
+ cameraId: CameraId,
+ requestOpen: RequestOpen,
+ ): RetrieveActiveCameraResult? {
+ var realCamera: ActiveCamera? = null
+ var realCameraToken: Token? = null
+ for (activeCamera in activeCameras) {
+ if (activeCamera.cameraId == cameraId) {
+ // Important: When we retrieve an active camera, there is a chance it has already
+ // reached its idle timeout, but we haven't processed the close request and remove
+ // it from the list. It's also possible that after we fetch this camera, this camera
+ // then gets closed in parallel by the idle timeout. Therefore, here we should
+ // acquire a token from this active camera to mark it as used and keep it opened.
+ val token = activeCamera.acquire()
+ if (token != null) {
+ realCamera = activeCamera
+ realCameraToken = token
+ break
+ } else {
+ // This ActiveCamera is already disconnected (i.e., WakeLock closed). Make sure
+ // the camera is closed before reopening.
+ activeCamera.close()
+ activeCamera.awaitClosed()
+ activeCameras.remove(activeCamera)
+ }
+ }
+ }
+ if (realCamera == null) {
+ val openResult =
+ openCameraWithRetry(
+ cameraId,
+ requestOpen.sharedCameraIds,
+ requestOpen.isForegroundObserver,
+ scope,
+ )
+ when (openResult) {
+ is OpenVirtualCameraResult.Success -> {
+ Log.info { "PruningCameraDeviceManager: $cameraId opened successfully" }
+ realCamera = openResult.activeCamera
+ // Acquire a token to mark this active camera as used.
+ realCameraToken = checkNotNull(realCamera.acquire())
+ activeCameras.add(realCamera)
+ }
+ is OpenVirtualCameraResult.Error -> {
+ Log.info { "PruningCameraDeviceManager: Failed to open $cameraId" }
+ requestOpen.virtualCamera.disconnect(openResult.lastCameraError)
+ return null
+ }
+ }
+ }
+ return RetrieveActiveCameraResult(realCamera, checkNotNull(realCameraToken))
+ }
+
private suspend fun openCameraWithRetry(
cameraId: CameraId,
sharedCameraIds: List<CameraId>,
@@ -509,23 +555,32 @@
)
}
- private suspend fun connectPendingRequestOpens(cameraIds: List<CameraId>) {
- val requestOpensToRemove = mutableListOf<RequestOpen>()
- val requestOpens =
- pendingRequestOpens.filter { cameraIds.contains(it.virtualCamera.cameraId) }
- for (request in requestOpens) {
- // If the request is shared with this pending request, then we should be
- // able to connect this pending request too, since we don't allow
- // overlapping.
+ private suspend fun connectPendingRequestOpens(cameraIds: Set<CameraId>) {
+ val filteredPendingRequestOpens =
+ pendingRequestOpens.filter { cameraIds.contains(it.request.virtualCamera.cameraId) }
+ for (pendingRequestOpen in filteredPendingRequestOpens) {
+ val request = pendingRequestOpen.request
+
+ // If the request is shared with this pending request, then we should be able to connect
+ // this pending request too, since we don't allow overlapping.
val allCameraIds = listOf(request.virtualCamera.cameraId) + request.sharedCameraIds
check(allCameraIds.all { cameraId -> activeCameras.any { it.cameraId == cameraId } })
- val realCamera = activeCameras.find { it.cameraId == request.virtualCamera.cameraId }
- checkNotNull(realCamera)
- realCamera.connectTo(request.virtualCamera)
- requestOpensToRemove.add(request)
+ pendingRequestOpen.activeCamera.connectTo(
+ request.virtualCamera,
+ pendingRequestOpen.token
+ )
+ pendingRequestOpens.remove(pendingRequestOpen)
}
- pendingRequestOpens.removeAll(requestOpensToRemove)
+ }
+
+ private suspend fun disconnectPendingRequestOpens(
+ pendingRequestOpensToDisconnect: List<PendingRequestOpen>,
+ ) {
+ for (pendingRequestOpen in pendingRequestOpensToDisconnect) {
+ pendingRequestOpen.token.release()
+ pendingRequestOpens.remove(pendingRequestOpen)
+ }
}
private inline fun <T> List<T>.firstFromIndexOrNull(
@@ -548,6 +603,8 @@
return removedElements
}
+ private class RetrieveActiveCameraResult(val activeCamera: ActiveCamera, val token: Token)
+
private sealed interface OpenVirtualCameraResult {
data class Success(val activeCamera: ActiveCamera) : OpenVirtualCameraResult
@@ -788,7 +845,7 @@
// If the camera of the request and the cameras it is shared with have been
// opened, we can connect the ActiveCameras.
check(!request.isPrewarm)
- realCamera.connectTo(request.virtualCamera)
+ realCamera.connectTo(request.virtualCamera, realCamera.acquire())
connectPendingRequestOpens(request.sharedCameraIds)
} else {
// Else, save the request in the pending request queue, and connect the request
@@ -797,7 +854,7 @@
}
} else {
if (!request.isPrewarm) {
- realCamera.connectTo(request.virtualCamera)
+ realCamera.connectTo(request.virtualCamera, realCamera.acquire())
}
}
requests.remove(request)
@@ -850,7 +907,7 @@
val realCamera = activeCameras.find { it.cameraId == request.virtualCamera.cameraId }
checkNotNull(realCamera)
- realCamera.connectTo(request.virtualCamera)
+ realCamera.connectTo(request.virtualCamera, realCamera.acquire())
requestOpensToRemove.add(request)
}
pendingRequestOpens.removeAll(requestOpensToRemove)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt
index cb7ac0b..d8b0924 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt
@@ -315,7 +315,7 @@
val virtualCamera1 =
deviceManager.open(cameraId0, listOf(cameraId1), fakeGraphListener1, false) { true }
assertNotNull(virtualCamera1)
- // Advance time by just a bit to allow coroutines to finish but not closing the camera.
+ // Advance time by just a bit to allow coroutines to finish.
advanceTimeBy(100)
assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 1)
@@ -344,6 +344,42 @@
}
@Test
+ fun pendingCamerasShouldBeHeldWhenOpeningConcurrentCameras() =
+ testScope.runTest {
+ val virtualCamera1 =
+ deviceManager.open(cameraId0, listOf(cameraId1), fakeGraphListener1, false) { true }
+ assertNotNull(virtualCamera1)
+ // Advance until idle. This is the only but notable difference between this and the
+ // prior test. Note that here because the request is still pending, the active camera
+ // should not be closed.
+ advanceUntilIdle()
+
+ assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 1)
+ val androidCameraState1 = fakeRetryingCameraStateOpener.androidCameraStates.first()
+ androidCameraState1.onOpened(fakeCameraDevice0)
+ advanceUntilIdle()
+
+ // Since camera 1 is not yet opened, the virtual camera should not be connected yet.
+ var virtualCameraState1 = virtualCamera1.value
+ assertIsNot<CameraStateOpen>(virtualCameraState1)
+
+ val virtualCamera2 =
+ deviceManager.open(cameraId1, listOf(cameraId0), fakeGraphListener2, false) { true }
+ assertNotNull(virtualCamera2)
+ advanceUntilIdle()
+
+ assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 2)
+ val androidCameraState2 = fakeRetryingCameraStateOpener.androidCameraStates.last()
+ androidCameraState2.onOpened(fakeCameraDevice1)
+ advanceUntilIdle()
+
+ virtualCameraState1 = virtualCamera1.value
+ assertIs<CameraStateOpen>(virtualCameraState1)
+ val virtualCameraState2 = virtualCamera2.value
+ assertIs<CameraStateOpen>(virtualCameraState2)
+ }
+
+ @Test
fun singleCameraShouldBeClosedWhenConcurrentCamerasAreRequested() =
testScope.runTest {
// First open camera 0 in regular (single) camera mode.
@@ -395,7 +431,7 @@
val virtualCamera1 =
deviceManager.open(cameraId0, listOf(cameraId1), fakeGraphListener1, false) { true }
assertNotNull(virtualCamera1)
- // Advance time by just a bit to allow coroutines to finish but not closing the camera.
+ // Advance time by just a bit to allow coroutines to finish.
advanceTimeBy(100)
assertEquals(fakeRetryingCameraStateOpener.androidCameraStates.size, 1)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
index de994f6..8bcdb8c 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
@@ -23,6 +23,7 @@
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.internal.util.TestUtil;
+import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraXConfig;
import androidx.camera.core.ImageAnalysis;
@@ -88,7 +89,8 @@
mCamera = CameraUtil.createCameraAndAttachUseCase(context, cameraSelector, imageAnalysis);
mCameraControl = TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
mTorchControl = mCameraControl.getTorchControl();
- mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel() > 1;
+ mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel()
+ != CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED;
}
@After
@@ -118,7 +120,7 @@
@Test(timeout = 5000L)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
- public void setTorchStrengthLevel_futureCompleteWhenTorchIsOnLevel()
+ public void setTorchStrengthLevel_futureCompleteWhenTorchIsOn()
throws ExecutionException, InterruptedException {
assumeTrue(mIsTorchStrengthSupported);
@@ -132,7 +134,7 @@
@Test(timeout = 5000L)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
- public void setTorchStrengthLevel_futureCompleteWhenTorchIsOffLevel()
+ public void setTorchStrengthLevel_futureCompleteWhenTorchIsOff()
throws ExecutionException, InterruptedException {
assumeTrue(mIsTorchStrengthSupported);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 8a2a439..d6321f2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -516,6 +516,10 @@
return Futures.immediateFailedFuture(
new OperationCanceledException("Camera is not active."));
}
+ if (!mCameraCharacteristics.isTorchStrengthLevelSupported()) {
+ return Futures.immediateFailedFuture(new UnsupportedOperationException(
+ "The device doesn't support configuring torch strength level."));
+ }
if (torchStrengthLevel < 1
|| torchStrengthLevel > mCameraCharacteristics.getMaxTorchStrengthLevel()) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index aeb7058..4804b521 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -719,9 +719,11 @@
}
@Override
- @IntRange(from = 1)
+ @IntRange(from = 0)
public int getMaxTorchStrengthLevel() {
- return mCameraCharacteristicsCompat.getMaxTorchStrengthLevel();
+ return mCameraCharacteristicsCompat.isTorchStrengthLevelSupported()
+ ? mCameraCharacteristicsCompat.getMaxTorchStrengthLevel()
+ : TORCH_STRENGTH_LEVEL_UNSUPPORTED;
}
@Override
@@ -730,7 +732,9 @@
if (mCamera2CameraControlImpl == null) {
if (mRedirectTorchStrengthLiveData == null) {
mRedirectTorchStrengthLiveData = new RedirectableLiveData<>(
- mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel());
+ mCameraCharacteristicsCompat.isTorchStrengthLevelSupported()
+ ? mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel()
+ : TORCH_STRENGTH_LEVEL_UNSUPPORTED);
}
return mRedirectTorchStrengthLiveData;
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 95d9e1c..49ef6d6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -16,7 +16,6 @@
package androidx.camera.camera2.internal;
-import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
@@ -68,8 +67,6 @@
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -1067,7 +1064,8 @@
continue;
}
List<OutputConfiguration> outputConfigurations =
- createInstancesForMultiResolutionOutput(streamInfos, imageFormat);
+ OutputConfiguration.createInstancesForMultiResolutionOutput(streamInfos,
+ imageFormat);
if (outputConfigurations != null) {
for (SessionConfig.OutputConfig outputConfig : groupIdToOutputConfigsMap.get(
groupId)) {
@@ -1082,31 +1080,6 @@
return outputConfigToOutputConfigurationCompatMap;
}
- /**
- * Use java reflection to access the API so that we don't need to upgrade compileSdk as 35 in
- * the release branch. When this method is invoked, the API has become public on the device. It
- * won't cause the problem about accessing the non-SDK API.
- */
- /** @noinspection unchecked */
- @SuppressLint("BanUncheckedReflection")
- @SuppressWarnings("unchecked")
- @RequiresApi(35)
- private static @Nullable List<OutputConfiguration> createInstancesForMultiResolutionOutput(
- @NonNull List<MultiResolutionStreamInfo> streamInfos, int format) {
- // TODO(b/376185185): Invoke the API directly after the androidx code base upgrades to
- // compile by API 35 SDK.
- try {
- Method createInstanceMethod = OutputConfiguration.class.getMethod(
- "createInstancesForMultiResolutionOutput", Collection.class, int.class);
- return (List<OutputConfiguration>) createInstanceMethod.invoke(null, streamInfos,
- format);
- } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
- Logger.e(TAG,
- "Failed to create instances for multi-resolution output, " + e.getMessage());
- return null;
- }
- }
-
// Debugging note: these states are kept in ordinal order. Any additions or changes should try
// to maintain the same order such that the highest ordinal is the state of largest resource
// utilization.
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index 7ea9db9..1bb729d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -103,10 +103,10 @@
mExecutor = executor;
mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
- mIsTorchStrengthSupported =
- mHasFlashUnit && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
- && cameraCharacteristics.getMaxTorchStrengthLevel() > 1;
- mDefaultTorchStrength = cameraCharacteristics.getDefaultTorchStrengthLevel();
+ mIsTorchStrengthSupported = cameraCharacteristics.isTorchStrengthLevelSupported();
+ mDefaultTorchStrength = mHasFlashUnit && mIsTorchStrengthSupported
+ ? cameraCharacteristics.getDefaultTorchStrengthLevel()
+ : Camera2CameraInfoImpl.TORCH_STRENGTH_LEVEL_UNSUPPORTED;
mTargetTorchStrength = mDefaultTorchStrength;
mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
mTorchStrength = new MutableLiveData<>(mDefaultTorchStrength);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index 1f2cc60..766cfec 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -22,6 +22,7 @@
import android.os.Build;
import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.internal.compat.workaround.OutputSizesCorrector;
@@ -135,23 +136,40 @@
*/
public int getDefaultTorchStrengthLevel() {
Integer defaultLevel = null;
- if (Build.VERSION.SDK_INT >= 35) {
+ if (hasFlashUnit() && Build.VERSION.SDK_INT >= 35) {
defaultLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL);
}
+ // The framework returns 1 when the device doesn't support configuring torch strength. So
+ // also return 1 if the device doesn't have flash unit or is unable to provide the
+ // information.
return defaultLevel == null ? 1 : defaultLevel;
}
/**
* Returns the maximum torch strength level.
*/
+ @IntRange(from = 1)
public int getMaxTorchStrengthLevel() {
Integer maxLevel = null;
- if (Build.VERSION.SDK_INT >= 35) {
+ if (hasFlashUnit() && Build.VERSION.SDK_INT >= 35) {
maxLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL);
}
+ // The framework returns 1 when the device doesn't support configuring torch strength. So
+ // also return 1 if the device doesn't have flash unit or is unable to provide the
+ // information.
return maxLevel == null ? 1 : maxLevel;
}
+ public boolean isTorchStrengthLevelSupported() {
+ return hasFlashUnit() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && getMaxTorchStrengthLevel() > 1;
+ }
+
+ private boolean hasFlashUnit() {
+ Boolean flashInfoAvailable = get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ return flashInfoAvailable != null && flashInfoAvailable;
+ }
+
/**
* Obtains the {@link StreamConfigurationMapCompat} which contains the output sizes related
* workarounds in it.
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index e7fcc70..1d87d00 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -916,6 +916,30 @@
assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(CAMERA0_MAX_TORCH_STRENGTH);
}
+ @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ public void apiVersionMet_canReturnMaxTorchStrengthUnsupported()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(
+ CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ public void apiVersionMet_canReturnTorchStrengthUnsupported()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(
+ CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
+ }
+
@Config(minSdk = 33)
@Test
public void apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
@@ -944,13 +968,14 @@
@Config(maxSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM - 1)
@Test
- public void apiVersionNotMet_returnMaxTorchStrengthOne()
+ public void apiVersionNotMet_returnMaxTorchStrengthUnsupported()
throws CameraAccessExceptionCompat {
init(/* hasAvailableCapabilities = */ true);
final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
- assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(1);
+ assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(
+ CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
}
@Config(maxSdk = 32)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
index d725f7f..9f45cd9 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
@@ -155,6 +155,17 @@
}
@Test
+ public void setTorchStrengthLevel_throwExceptionWhenNoFlashUnit() throws InterruptedException {
+ Throwable cause = null;
+ try {
+ mNoFlashUnitTorchControl.setTorchStrengthLevel(1).get();
+ } catch (ExecutionException e) {
+ cause = e.getCause();
+ }
+ assertThat(cause).isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ @Test
public void enableTorch_whenInactive() throws InterruptedException {
mTorchControl.setActive(false);
ListenableFuture<Void> listenableFuture = mTorchControl.enableTorch(true);
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 5c0933e..893b313 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -49,7 +49,7 @@
method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
method public default int getLensFacing();
method public default androidx.lifecycle.LiveData<java.lang.Integer!> getLowLightBoostState();
- method @IntRange(from=1) public default int getMaxTorchStrengthLevel();
+ method @IntRange(from=0) public default int getMaxTorchStrengthLevel();
method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
method public int getSensorRotationDegrees();
method public int getSensorRotationDegrees(int);
@@ -64,6 +64,7 @@
method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
method public static boolean mustPlayShutterSound();
method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+ field public static final int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0; // 0x0
}
public final class CameraInfoUnavailableException extends java.lang.Exception {
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 5c0933e..893b313 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -49,7 +49,7 @@
method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
method public default int getLensFacing();
method public default androidx.lifecycle.LiveData<java.lang.Integer!> getLowLightBoostState();
- method @IntRange(from=1) public default int getMaxTorchStrengthLevel();
+ method @IntRange(from=0) public default int getMaxTorchStrengthLevel();
method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
method public int getSensorRotationDegrees();
method public int getSensorRotationDegrees(int);
@@ -64,6 +64,7 @@
method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
method public static boolean mustPlayShutterSound();
method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+ field public static final int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0; // 0x0
}
public final class CameraInfoUnavailableException extends java.lang.Exception {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index ac335d1..2332ad6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -245,6 +245,10 @@
* {@link ListenableFuture} will fail with an {@link IllegalArgumentException} and it won't
* modify the torch strength.
*
+ * <p>If the device doesn't have a flash unit or doesn't support configuring torch strength
+ * level, the returned {@link ListenableFuture} will fail with an
+ * {@link UnsupportedOperationException}.
+ *
* @param torchStrengthLevel The desired torch strength level.
* @return a {@link ListenableFuture} that is completed when the torch strength has been
* applied.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 52a744f..cd2f8a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -50,6 +50,12 @@
public interface CameraInfo {
/**
+ * The torch strength level when the device doesn't have a flash unit or doesn't support
+ * adjusting torch strength.
+ */
+ int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0;
+
+ /**
* An unknown intrinsic zoom ratio. Usually to indicate the camera is unable to provide
* necessary information to resolve its intrinsic zoom ratio.
*
@@ -429,12 +435,12 @@
/**
* Returns the maximum torch strength level.
*
- * @return The maximum strength level, or {code 1} if the device doesn't have a flash unit or
- * doesn't support configuring torch strength.
+ * @return The maximum strength level, or {@link #TORCH_STRENGTH_LEVEL_UNSUPPORTED} if the
+ * device doesn't have a flash unit or doesn't support configuring torch strength.
*/
- @IntRange(from = 1)
+ @IntRange(from = 0)
default int getMaxTorchStrengthLevel() {
- return 1;
+ return TORCH_STRENGTH_LEVEL_UNSUPPORTED;
}
/**
@@ -442,9 +448,12 @@
*
* <p>The value of the {@link LiveData} will be the default torch strength level of this
* device if {@link CameraControl#setTorchStrengthLevelAsync(int)} hasn't been called.
+ *
+ * <p>The value of the {@link LiveData} will be {@link #TORCH_STRENGTH_LEVEL_UNSUPPORTED} if
+ * the device doesn't have a flash unit or doesn't support configuring torch strength.
*/
default @NonNull LiveData<Integer> getTorchStrengthLevel() {
- return new MutableLiveData<>(1);
+ return new MutableLiveData<>(TORCH_STRENGTH_LEVEL_UNSUPPORTED);
}
@StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index 10d896a..de7943c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -19,6 +19,7 @@
import android.util.Range;
import android.util.Size;
+import androidx.annotation.IntRange;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraState;
@@ -78,6 +79,7 @@
}
@Override
+ @IntRange(from = 0)
public int getMaxTorchStrengthLevel() {
return mCameraInfoInternal.getMaxTorchStrengthLevel();
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index befe1c0..c3aa6d1 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -365,13 +365,13 @@
fun getResolutionInfo_shouldMatchRecordedVideoResolution() {
// Arrange.
checkAndBindUseCases(preview, videoCapture)
+ val resolutionInfo = videoCapture.resolutionInfo!!
// Act.
val result = recordingSession.createRecording().recordAndVerify()
// Assert: the resolution of the video file should match the resolution calculated by
// rotating the cropRect specified in the ResolutionInfo.
- val resolutionInfo = videoCapture.resolutionInfo!!
val expectedResolution =
rotateSize(rectToSize(resolutionInfo.cropRect), resolutionInfo.rotationDegrees)
verifyVideoResolution(context, result.file, expectedResolution)
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 19331ab..de52e38 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -1322,7 +1322,7 @@
Bundle bundle = this.getIntent().getExtras();
if (bundle != null) {
mTargetAspectRatio = bundle.getInt(INTENT_EXTRA_TARGET_ASPECT_RATIO,
- AspectRatio.RATIO_4_3);
+ AspectRatio.RATIO_DEFAULT);
int scaleType = bundle.getInt(INTENT_EXTRA_SCALE_TYPE, INTENT_EXTRA_FILL_CENTER);
if (scaleType == INTENT_EXTRA_FIT_CENTER) {
// Scale the view according to the target aspect ratio, display size and device
diff --git a/car/app/app/src/main/java/androidx/car/app/CarToast.java b/car/app/app/src/main/java/androidx/car/app/CarToast.java
index 0df1034..97bc2a4b 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarToast.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarToast.java
@@ -72,6 +72,7 @@
/**
* Creates and sets the text and duration for the toast view.
*
+ * @param carContext the context used for string localization
* @param textResId the resource id for the text to show. If the {@code textResId} is 0, the
* text will be set to empty
* @param duration how long to display the message. Either {@link #LENGTH_SHORT} or {@link
@@ -89,6 +90,7 @@
/**
* Creates and sets the text and duration for the toast view.
*
+ * @param carContext the CarContext providing the service used to show the toast
* @param text the text to show
* @param duration how long to display the message. Either {@link #LENGTH_SHORT} or {@link
* #LENGTH_LONG}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java b/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java
index b104443..73f61f3 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/AlertCallbackDelegate.java
@@ -34,6 +34,7 @@
/**
* Notifies that a cancel event happened with given {@code reason}.
*
+ * @param reason the {@link AlertCallback.Reason} for which the alert was cancelled
* @param callback the {@link OnDoneCallback} to trigger when the client finishes handling
* the event
*/
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index 78ac040..7f8b8f7 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.input
+import android.R
import android.os.Build
import android.text.InputType
import android.text.SpannableStringBuilder
@@ -40,7 +41,10 @@
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.computeSizeForDefaultText
import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
+import androidx.compose.foundation.text.input.internal.TextLayoutState
+import androidx.compose.foundation.text.input.internal.TransformedTextFieldState
import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
+import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
import androidx.compose.foundation.text.input.internal.setComposingRegion
import androidx.compose.foundation.text.selection.fetchTextLayoutResult
import androidx.compose.foundation.verticalScroll
@@ -127,6 +131,9 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
@LargeTest
@@ -1130,9 +1137,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection {
- performContextMenuAction(android.R.id.selectAll)
- }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.selectAll) }
rule.runOnIdle {
assertThat(state.selection).isEqualTo(TextRange(0, 5))
@@ -1152,7 +1157,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.cut) }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.cut) }
rule.waitForIdle()
assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1171,7 +1176,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.copy) }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.copy) }
rule.waitForIdle()
assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1189,7 +1194,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.paste) }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.paste) }
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Worldo")
@@ -1299,6 +1304,34 @@
}
@Test
+ fun textField_state_invokesAutofill() {
+ val mockLambda: () -> Unit = mock()
+ var density by mutableStateOf(Density(1f))
+
+ val manager =
+ TextFieldSelectionState(
+ // other parameters not necessary to test autofill invocation
+ textFieldState =
+ TransformedTextFieldState(
+ textFieldState = TextFieldState(),
+ inputTransformation = null,
+ codepointTransformation = null,
+ outputTransformation = null
+ ),
+ textLayoutState = TextLayoutState(),
+ density = density,
+ enabled = true,
+ readOnly = false,
+ isFocused = false,
+ isPassword = false
+ )
+ .apply { requestAutofillAction = mockLambda }
+
+ manager.autofill()
+ verify(mockLambda, times(1)).invoke()
+ }
+
+ @Test
fun changingInputTransformation_doesNotRestartInput() {
var inputTransformation by mutableStateOf(InputTransformation.maxLength(10))
inputMethodInterceptor.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
index a90d80a..50eb946 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
@@ -30,6 +30,7 @@
import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -854,6 +855,38 @@
}
}
+ @Test
+ fun textField_keyEvent_functionReference() {
+ val state = mutableIntStateOf(0)
+ var handled = -1
+ val focusRequester = FocusRequester()
+ rule.setContent {
+ val stateValue = state.value
+
+ @Suppress("UNUSED_PARAMETER")
+ fun handle(key: KeyEvent): Boolean {
+ handled = stateValue
+ return true
+ }
+
+ BasicTextField(
+ value = "text",
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester).testTag(tag).onKeyEvent(::handle)
+ )
+ }
+
+ rule.runOnIdle { focusRequester.requestFocus() }
+ rule.onNodeWithTag(tag).performKeyInput { pressKey(Key.A) }
+ rule.runOnIdle {
+ assertThat(handled).isEqualTo(0)
+ state.value += 1
+ }
+
+ rule.onNodeWithTag(tag).performKeyInput { pressKey(Key.A) }
+ rule.runOnIdle { assertThat(handled).isEqualTo(1) }
+ }
+
private inner class SequenceScope(
val state: TextFieldState,
val clipboard: Clipboard,
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index fb28556..9d8e33e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -23,7 +23,6 @@
import androidx.compose.foundation.text.LegacyTextFieldState
import androidx.compose.foundation.text.TextDelegate
import androidx.compose.foundation.text.TextLayoutResultProxy
-import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -98,7 +97,6 @@
private val hapticFeedback = mock<HapticFeedback>()
private val focusRequester = mock<FocusRequester>()
private val multiParagraph = mock<MultiParagraph>()
- private val autofillManager = mock<AutofillManager>()
@Before
fun setup() {
@@ -110,7 +108,6 @@
manager.textToolbar = textToolbar
manager.hapticFeedBack = hapticFeedback
manager.focusRequester = focusRequester
- manager.autofillManager = autofillManager
manager.coroutineScope = null
whenever(layoutResult.layoutInput)
@@ -370,10 +367,12 @@
@Test
fun autofill_selection_collapse() {
manager.value = TextFieldValue(text = text, selection = TextRange(4, 4))
+ val mockLambda: () -> Unit = mock()
+ val manager = TextFieldSelectionManager().apply { requestAutofillAction = mockLambda }
manager.autofill()
- verify(autofillManager, times(1)).requestAutofillForActiveElement()
+ verify(mockLambda, times(1)).invoke()
assertThat(state.handleState).isEqualTo(HandleState.None)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 925ecfd..7e63192 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -68,7 +68,6 @@
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
@@ -301,7 +300,6 @@
val currentHapticFeedback = LocalHapticFeedback.current
val currentClipboard = LocalClipboard.current
val currentTextToolbar = LocalTextToolbar.current
- val autofillManager = LocalAutofillManager.current
val textToolbarHandler =
remember(coroutineScope, currentTextToolbar) {
@@ -360,7 +358,6 @@
enabled = enabled,
readOnly = readOnly,
isPassword = isPassword,
- autofillManager = autofillManager,
showTextToolbar = textToolbarHandler
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 14f8402..494f392 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,6 +27,7 @@
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.text.handwriting.stylusHandwriting
+import androidx.compose.foundation.text.input.internal.CoreTextFieldSemanticsModifier
import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -58,7 +59,6 @@
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.ContentDataType
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
@@ -82,7 +82,6 @@
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
@@ -92,33 +91,13 @@
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.semantics.contentDataType
-import androidx.compose.ui.semantics.copyText
-import androidx.compose.ui.semantics.cutText
-import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.editableText
-import androidx.compose.ui.semantics.getTextLayoutResult
-import androidx.compose.ui.semantics.insertTextAtCursor
-import androidx.compose.ui.semantics.isEditable
-import androidx.compose.ui.semantics.onAutofillText
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.onImeAction
-import androidx.compose.ui.semantics.onLongClick
-import androidx.compose.ui.semantics.password
-import androidx.compose.ui.semantics.pasteText
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.setSelection
-import androidx.compose.ui.semantics.setText
-import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.input.CommitTextCommand
-import androidx.compose.ui.text.input.DeleteAllCommand
import androidx.compose.ui.text.input.EditProcessor
-import androidx.compose.ui.text.input.FinishComposingTextCommand
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardType
@@ -316,7 +295,6 @@
manager.coroutineScope = coroutineScope
manager.textToolbar = LocalTextToolbar.current
manager.hapticFeedBack = LocalHapticFeedback.current
- manager.autofillManager = LocalAutofillManager.current
manager.focusRequester = focusRequester
manager.editable = !readOnly
manager.enabled = enabled
@@ -474,151 +452,18 @@
val isPassword = visualTransformation is PasswordVisualTransformation
val semanticsModifier =
- Modifier.semantics(true) {
- // focused semantics are handled by Modifier.focusable()
- this.editableText = transformedText.text
- this.textSelectionRange = value.selection
-
- // The developer will set `contentType`. CTF populates the other autofill-related
- // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
- this.contentDataType = ContentDataType.Text
- onAutofillText { text ->
- state.justAutofilled = true
- state.autofillHighlightOn = true
- handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
- true
- }
-
- if (!enabled) this.disabled()
- if (isPassword) this.password()
- val editable = enabled && !readOnly
- isEditable = editable
- getTextLayoutResult {
- if (state.layoutResult != null) {
- it.add(state.layoutResult!!.value)
- true
- } else {
- false
- }
- }
- if (editable) {
- setText { text ->
- handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
- true
- }
-
- insertTextAtCursor { text ->
- if (readOnly || !enabled) return@insertTextAtCursor false
-
- // If the action is performed while in an active text editing session, treat
- // this like an IME command and update the text by going through the buffer.
- // This keeps the buffer state consistent if other IME commands are performed
- // before the next recomposition, and is used for the testing code path.
- state.inputSession?.let { session ->
- TextFieldDelegate.onEditCommand(
- // Finish composing text first because when the field is focused the IME
- // might
- // set composition.
- ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
- editProcessor = state.processor,
- state.onValueChange,
- session
- )
- }
- ?: run {
- val newText =
- value.text.replaceRange(
- value.selection.start,
- value.selection.end,
- text
- )
- val newCursor = TextRange(value.selection.start + text.length)
- state.onValueChange(TextFieldValue(newText, newCursor))
- }
- true
- }
- }
-
- setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
- // in traversal mode we get selection from the `textSelectionRange` semantics which
- // is
- // selection in original text. In non-traversal mode selection comes from the
- // Talkback
- // and indices are relative to the transformed text
- val start =
- if (relativeToOriginalText) {
- selectionStart
- } else {
- offsetMapping.transformedToOriginal(selectionStart)
- }
- val end =
- if (relativeToOriginalText) {
- selectionEnd
- } else {
- offsetMapping.transformedToOriginal(selectionEnd)
- }
-
- if (!enabled) {
- false
- } else if (start == value.selection.start && end == value.selection.end) {
- false
- } else if (
- minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
- ) {
- // Do not show toolbar if it's a traversal mode (with the volume keys), or
- // if the cursor just moved to beginning or end.
- if (relativeToOriginalText || start == end) {
- manager.exitSelectionMode()
- } else {
- manager.enterSelectionMode()
- }
- state.onValueChange(
- TextFieldValue(value.annotatedString, TextRange(start, end))
- )
- true
- } else {
- manager.exitSelectionMode()
- false
- }
- }
- onImeAction(imeOptions.imeAction) {
- // This will perform the appropriate default action if no handler has been
- // specified, so
- // as far as the platform is concerned, we always handle the action and never want
- // to
- // defer to the default _platform_ implementation.
- state.onImeActionPerformed(imeOptions.imeAction)
- true
- }
- onClick {
- // according to the documentation, we still need to provide proper semantics actions
- // even if the state is 'disabled'
- tapToFocus(state, focusRequester, !readOnly)
- true
- }
- onLongClick {
- manager.enterSelectionMode()
- true
- }
- if (!value.selection.collapsed && !isPassword) {
- copyText {
- manager.copy()
- true
- }
- if (enabled && !readOnly) {
- cutText {
- manager.cut()
- true
- }
- }
- }
- if (enabled && !readOnly) {
- pasteText {
- manager.paste()
- true
- }
- }
- }
+ CoreTextFieldSemanticsModifier(
+ transformedText,
+ value,
+ state,
+ readOnly,
+ enabled,
+ isPassword,
+ offsetMapping,
+ manager,
+ imeOptions,
+ focusRequester
+ )
val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight()
val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorBrush, showCursor)
@@ -884,30 +729,6 @@
}
}
-/**
- * In an active input session, semantics updates are handled just as user updates coming from the
- * IME. Otherwise the updates are directly applied on the current state.
- */
-private fun handleTextUpdateFromSemantics(
- state: LegacyTextFieldState,
- text: String,
- readOnly: Boolean,
- enabled: Boolean
-) {
- if (readOnly || !enabled) return
-
- // If the action is performed while in an active text editing session, treat this
- // like an IME command and update the text by going through the buffer.
- state.inputSession?.let { session ->
- TextFieldDelegate.onEditCommand(
- ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
- editProcessor = state.processor,
- state.onValueChange,
- session
- )
- } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
-}
-
internal class LegacyTextFieldState(
var textDelegate: TextDelegate,
val recomposeScope: RecomposeScope,
@@ -1108,7 +929,7 @@
}
/** Request focus on tap. If already focused, makes sure the keyboard is requested. */
-private fun tapToFocus(
+internal fun tapToFocus(
state: LegacyTextFieldState,
focusRequester: FocusRequester,
allowKeyboard: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
new file mode 100644
index 0000000..a5fe111
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.input.internal
+
+import androidx.compose.foundation.text.LegacyTextFieldState
+import androidx.compose.foundation.text.TextFieldDelegate
+import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.foundation.text.tapToFocus
+import androidx.compose.ui.autofill.ContentDataType
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.node.requestAutofill
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.contentDataType
+import androidx.compose.ui.semantics.copyText
+import androidx.compose.ui.semantics.cutText
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.editableText
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.insertTextAtCursor
+import androidx.compose.ui.semantics.isEditable
+import androidx.compose.ui.semantics.onAutofillText
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.onImeAction
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.password
+import androidx.compose.ui.semantics.pasteText
+import androidx.compose.ui.semantics.setSelection
+import androidx.compose.ui.semantics.setText
+import androidx.compose.ui.semantics.textSelectionRange
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.DeleteAllCommand
+import androidx.compose.ui.text.input.FinishComposingTextCommand
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TransformedText
+
+internal data class CoreTextFieldSemanticsModifier(
+ val transformedText: TransformedText,
+ val value: TextFieldValue,
+ val state: LegacyTextFieldState,
+ val readOnly: Boolean,
+ val enabled: Boolean,
+ val isPassword: Boolean,
+ val offsetMapping: OffsetMapping,
+ val manager: TextFieldSelectionManager,
+ val imeOptions: ImeOptions,
+ val focusRequester: FocusRequester
+) : ModifierNodeElement<CoreTextFieldSemanticsModifierNode>() {
+ override fun create(): CoreTextFieldSemanticsModifierNode =
+ CoreTextFieldSemanticsModifierNode(
+ transformedText = transformedText,
+ value = value,
+ state = state,
+ readOnly = readOnly,
+ enabled = enabled,
+ isPassword = isPassword,
+ offsetMapping = offsetMapping,
+ manager = manager,
+ imeOptions = imeOptions,
+ focusRequester = focusRequester
+ )
+
+ override fun update(node: CoreTextFieldSemanticsModifierNode) {
+ node.updateNodeSemantics(
+ transformedText = transformedText,
+ value = value,
+ state = state,
+ readOnly = readOnly,
+ enabled = enabled,
+ isPassword = isPassword,
+ offsetMapping = offsetMapping,
+ manager = manager,
+ imeOptions = imeOptions,
+ focusRequester = focusRequester
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ // Show nothing in the inspector.
+ }
+}
+
+internal class CoreTextFieldSemanticsModifierNode(
+ var transformedText: TransformedText,
+ var value: TextFieldValue,
+ var state: LegacyTextFieldState,
+ var readOnly: Boolean,
+ var enabled: Boolean,
+ var isPassword: Boolean,
+ var offsetMapping: OffsetMapping,
+ var manager: TextFieldSelectionManager,
+ var imeOptions: ImeOptions,
+ var focusRequester: FocusRequester
+) : DelegatingNode(), SemanticsModifierNode {
+ init {
+ manager.requestAutofillAction = { requestAutofill() }
+ }
+
+ override val shouldMergeDescendantSemantics: Boolean
+ get() = true
+
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ this.editableText = transformedText.text
+ this.textSelectionRange = value.selection
+
+ // The developer will set `contentType`. CTF populates the other autofill-related
+ // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
+ this.contentDataType = ContentDataType.Text
+ onAutofillText { text ->
+ state.justAutofilled = true
+ state.autofillHighlightOn = true
+ handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+ true
+ }
+
+ if (!enabled) this.disabled()
+ if (isPassword) this.password()
+ val editable = enabled && !readOnly
+ isEditable = editable
+ getTextLayoutResult {
+ if (state.layoutResult != null) {
+ it.add(state.layoutResult!!.value)
+ true
+ } else {
+ false
+ }
+ }
+
+ if (editable) {
+ setText { text ->
+ handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+ true
+ }
+
+ insertTextAtCursor { text ->
+ if (readOnly || !enabled) return@insertTextAtCursor false
+
+ // If the action is performed while in an active text editing session, treat
+ // this like an IME command and update the text by going through the buffer.
+ // This keeps the buffer state consistent if other IME commands are performed
+ // before the next recomposition, and is used for the testing code path.
+ state.inputSession?.let { session ->
+ TextFieldDelegate.onEditCommand(
+ // Finish composing text first because when the field is focused the IME
+ // might
+ // set composition.
+ ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
+ editProcessor = state.processor,
+ state.onValueChange,
+ session
+ )
+ }
+ ?: run {
+ val newText =
+ value.text.replaceRange(
+ value.selection.start,
+ value.selection.end,
+ text
+ )
+ val newCursor = TextRange(value.selection.start + text.length)
+ state.onValueChange(TextFieldValue(newText, newCursor))
+ }
+ true
+ }
+ }
+
+ setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
+ // in traversal mode we get selection from the `textSelectionRange` semantics which
+ // is selection in original text. In non-traversal mode selection comes from the
+ // Talkback and indices are relative to the transformed text
+ val start =
+ if (relativeToOriginalText) {
+ selectionStart
+ } else {
+ offsetMapping.transformedToOriginal(selectionStart)
+ }
+ val end =
+ if (relativeToOriginalText) {
+ selectionEnd
+ } else {
+ offsetMapping.transformedToOriginal(selectionEnd)
+ }
+
+ if (!enabled) {
+ false
+ } else if (start == value.selection.start && end == value.selection.end) {
+ false
+ } else if (
+ minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
+ ) {
+ // Do not show toolbar if it's a traversal mode (with the volume keys), or
+ // if the cursor just moved to beginning or end.
+ if (relativeToOriginalText || start == end) {
+ manager.exitSelectionMode()
+ } else {
+ manager.enterSelectionMode()
+ }
+ state.onValueChange(TextFieldValue(value.annotatedString, TextRange(start, end)))
+ true
+ } else {
+ manager.exitSelectionMode()
+ false
+ }
+ }
+ onImeAction(imeOptions.imeAction) {
+ // This will perform the appropriate default action if no handler has been
+ // specified, so
+ // as far as the platform is concerned, we always handle the action and never want
+ // to
+ // defer to the default _platform_ implementation.
+ state.onImeActionPerformed(imeOptions.imeAction)
+ true
+ }
+ onClick {
+ // according to the documentation, we still need to provide proper semantics actions
+ // even if the state is 'disabled'
+ tapToFocus(state, focusRequester, !readOnly)
+ true
+ }
+ onLongClick {
+ manager.enterSelectionMode()
+ true
+ }
+ if (!value.selection.collapsed && !isPassword) {
+ copyText {
+ manager.copy()
+ true
+ }
+ if (enabled && !readOnly) {
+ cutText {
+ manager.cut()
+ true
+ }
+ }
+ }
+ if (enabled && !readOnly) {
+ pasteText {
+ manager.paste()
+ true
+ }
+ }
+ }
+
+ fun updateNodeSemantics(
+ transformedText: TransformedText,
+ value: TextFieldValue,
+ state: LegacyTextFieldState,
+ readOnly: Boolean,
+ enabled: Boolean,
+ isPassword: Boolean,
+ offsetMapping: OffsetMapping,
+ manager: TextFieldSelectionManager,
+ imeOptions: ImeOptions,
+ focusRequester: FocusRequester
+ ) {
+ // Find the diff: current previous and new values before updating current.
+ val previousEditable = this.enabled && !this.readOnly
+ val previousEnabled = this.enabled
+ val previousIsPassword = this.isPassword
+ val previousImeOptions = this.imeOptions
+ val previousManager = this.manager
+ val editable = enabled && !readOnly
+
+ // Apply the diff.
+ this.transformedText = transformedText
+ this.value = value
+ this.state = state
+ this.readOnly = readOnly
+ this.enabled = enabled
+ this.offsetMapping = offsetMapping
+ this.manager = manager
+ this.imeOptions = imeOptions
+ this.focusRequester = focusRequester
+
+ if (
+ enabled != previousEnabled ||
+ editable != previousEditable ||
+ imeOptions != previousImeOptions ||
+ isPassword != previousIsPassword ||
+ !value.selection.collapsed
+ ) {
+ invalidateSemantics()
+ }
+
+ if (manager != previousManager) {
+ manager.requestAutofillAction = { requestAutofill() }
+ }
+ }
+
+ /**
+ * In an active input session, semantics updates are handled just as user updates coming from
+ * the IME. Otherwise the updates are directly applied on the current state.
+ */
+ private fun handleTextUpdateFromSemantics(
+ state: LegacyTextFieldState,
+ text: String,
+ readOnly: Boolean,
+ enabled: Boolean
+ ) {
+ if (readOnly || !enabled) return
+
+ // If the action is performed while in an active text editing session, treat this
+ // like an IME command and update the text by going through the buffer.
+ state.inputSession?.let { session ->
+ TextFieldDelegate.onEditCommand(
+ ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
+ editProcessor = state.processor,
+ state.onValueChange,
+ session
+ )
+ } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index c9eb589..d7810f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -66,6 +66,7 @@
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.invalidateSemantics
import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requestAutofill
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -202,8 +203,9 @@
ObserverModifierNode,
LayoutAwareModifierNode {
- private val editable
- get() = enabled && !readOnly
+ init {
+ textFieldSelectionState.requestAutofillAction = { requestAutofill() }
+ }
private val pointerInputNode =
delegate(
@@ -430,9 +432,7 @@
stylusHandwritingTrigger: MutableSharedFlow<Unit>?
) {
// Find the diff: current previous and new values before updating current.
- val previousEditable = this.editable
- val editable = enabled && !readOnly
-
+ val previousEditable = this.enabled && !this.readOnly
val previousEnabled = this.enabled
val previousTextFieldState = this.textFieldState
val previousKeyboardOptions = this.keyboardOptions
@@ -440,6 +440,7 @@
val previousInteractionSource = this.interactionSource
val previousIsPassword = this.isPassword
val previousStylusHandwritingTrigger = this.stylusHandwritingTrigger
+ val editable = enabled && !readOnly
// Apply the diff.
this.textFieldState = textFieldState
@@ -487,6 +488,7 @@
textFieldSelectionState.receiveContentConfiguration =
receiveContentConfigurationProvider
}
+ textFieldSelectionState.requestAutofillAction = { requestAutofill() }
}
if (interactionSource != previousInteractionSource) {
@@ -507,7 +509,8 @@
if (!enabled) disabled()
if (isPassword) password()
- isEditable = [email protected]
+ val editable = enabled && !readOnly
+ isEditable = editable
// The developer will set `contentType`. TF populates the other autofill-related
// semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
@@ -630,6 +633,7 @@
isElementFocused = focusState.isFocused
onFocusChange()
+ val editable = enabled && !readOnly
if (focusState.isFocused) {
// Deselect when losing focus even if readonly.
if (editable) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 5baa1a8..0a6c258 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -66,7 +66,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.isSpecified
@@ -115,9 +114,6 @@
var isFocused: Boolean,
private var isPassword: Boolean,
) {
- /** [AutofillManager] to perform Autofill. */
- private var autofillManager: AutofillManager? = null
-
/** [HapticFeedback] handle to perform haptic feedback. */
private var hapticFeedBack: HapticFeedback? = null
@@ -130,6 +126,9 @@
/** Whether user is interacting with the UI in touch mode. */
var isInTouchMode: Boolean by mutableStateOf(true)
+ /** The action to invoke when autofill is requested in text toolbar. */
+ var requestAutofillAction: (() -> Unit)? = null
+
/**
* Reduced [ReceiveContentConfiguration] from the attached modifier node hierarchy. This value
* is set by [TextFieldDecoratorModifierNode].
@@ -353,8 +352,7 @@
density: Density,
enabled: Boolean,
readOnly: Boolean,
- isPassword: Boolean,
- autofillManager: AutofillManager?
+ isPassword: Boolean
) {
if (!enabled) {
hideTextToolbar()
@@ -366,7 +364,6 @@
this.enabled = enabled
this.readOnly = readOnly
this.isPassword = isPassword
- this.autofillManager = autofillManager
}
/** Implements the complete set of gestures supported by the cursor handle. */
@@ -443,7 +440,6 @@
clipboard = null
hapticFeedBack = null
- autofillManager = null
}
/**
@@ -1395,7 +1391,7 @@
* Inserts credentials (if there exist any that match this field type) into the text field.
*/
fun autofill() {
- autofillManager?.requestAutofillForActiveElement()
+ requestAutofillAction?.invoke()
}
fun deselect() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index 363a279..a47838d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -62,6 +62,8 @@
autoSize: TextAutoSize? = null,
private var onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)? = null
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
+ override val shouldAutoInvalidate: Boolean
+ get() = false
private val textAnnotatedStringNode =
delegate(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 8a5c009..e8543a8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -82,6 +82,9 @@
private var autoSize: TextAutoSize? = null,
private var onShowTranslation: ((TextSubstitutionValue) -> Unit)? = null
) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
@Suppress("PrimitiveInCollection")
private var baselineCache: MutableMap<AlignmentLine, Int>? = null
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index a8dd10b..e561553 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -77,6 +77,9 @@
private var minLines: Int = DefaultMinLines,
private var overrideColor: ColorProducer? = null
) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+ override val shouldAutoInvalidate: Boolean
+ get() = false
+
@Suppress("PrimitiveInCollection") // Map required for use in public API.
// Usages of this collection are so few that the gains of using
// MutableObjectIntMap<AlignmentLine> and then converting to a Map<AlignmentLine, Int>
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 674b87a..6f75364 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -38,7 +38,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -100,8 +99,8 @@
*/
internal var visualTransformation: VisualTransformation = VisualTransformation.None
- /** [AutofillManager] to perform clipboard features. */
- internal var autofillManager: AutofillManager? = null
+ /** The action to invoke when autofill is requested in text toolbar. */
+ internal var requestAutofillAction: (() -> Unit)? = null
/** [Clipboard] to perform clipboard features. */
internal var clipboard: Clipboard? = null
@@ -705,7 +704,7 @@
}
internal fun autofill() {
- autofillManager?.requestAutofillForActiveElement()
+ requestAutofillAction?.invoke()
}
internal fun getHandlePosition(isStartHandle: Boolean): Offset {
diff --git a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt
new file mode 100644
index 0000000..828090b
--- /dev/null
+++ b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.ripple
+
+import android.graphics.RenderNode
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Test for [RippleHostView] */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RippleHostViewTest {
+
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ /**
+ * Test for b/377222399
+ *
+ * Note, without the corresponding fix this test would only fail on Samsung devices, unless
+ * manually changing RippleDrawable.mRippleStyle.mRippleStyle to STYLE_SOLID through reflection.
+ */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun doesNotDrawWhileUnattached() {
+ rule.runOnUiThread {
+ val activity = rule.activity
+
+ // View is explicitly not attached
+ val rippleHostView = RippleHostView(activity)
+
+ // Add a ripple while unattached
+ rippleHostView.addRipple(
+ PressInteraction.Press(Offset.Zero),
+ true,
+ Size(100f, 100f),
+ radius = 10,
+ color = Color.Red,
+ alpha = 0.4f,
+ onInvalidateRipple = {}
+ )
+
+ // Create a hardware backed canvas
+ val canvas = RenderNode("RippleHostViewTest").beginRecording()
+
+ // Should not crash
+ rippleHostView.draw(canvas)
+ }
+ }
+}
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
index d1f3bf4..ef949d5 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
@@ -52,6 +53,15 @@
// noop
}
+ override fun draw(canvas: Canvas) {
+ if (!isAttachedToWindow) {
+ // Cleanup any existing ripples if we added a ripple after being detached b/377222399
+ disposeRipple()
+ return
+ }
+ super.draw(canvas)
+ }
+
override fun refreshDrawableState() {
// We don't want the View to manage the drawable state, so avoid updating the ripple's
// state (via View.mBackground) when we lose window focus, or other events.
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
index 68e6215..d2c14a0 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/TextTest.kt
@@ -18,11 +18,13 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertTextEquals
@@ -112,6 +114,25 @@
}
@Test
+ fun testChangingFontSizeDoesNotInvalidateSemantics() {
+ val fontSize = mutableStateOf(16.sp)
+ var count = 0
+ val countModifier = Modifier.semantics { count++ }
+ rule.setContent {
+ ProvideTextStyle(ExpectedTextStyle) {
+ Box(Modifier.background(Color.White)) {
+ Text(modifier = countModifier, text = TestText, fontSize = fontSize.value)
+ }
+ }
+ }
+ rule.runOnIdle {
+ count = 0
+ fontSize.value = 20.sp
+ }
+ rule.runOnIdle { assertThat(count).isEqualTo(0) }
+ }
+
+ @Test
fun settingCustomTextStyle() {
var textColor: Color? = null
var textAlign: TextAlign? = null
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.ignore b/compose/material3/adaptive/adaptive-layout/api/current.ignore
new file mode 100644
index 0000000..6562a08
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ParameterNameChange: androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem#ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole, T) parameter #1:
+ Attempted to change parameter name from content to contentKey in constructor androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index d075899..b4cd5c9 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -94,7 +94,7 @@
property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
+ @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
}
@@ -110,12 +110,28 @@
property @androidx.compose.runtime.Composable public abstract String description;
}
- public static final class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
- ctor public PaneExpansionAnchor.Offset(float offset);
- method @androidx.compose.runtime.Composable public String getDescription();
- method public float getOffset();
- property @androidx.compose.runtime.Composable public String description;
+ public abstract static class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
+ method public final int getDirection();
+ method public final float getOffset();
+ property public final int direction;
property public final float offset;
+ field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Companion Companion;
+ }
+
+ public static final class PaneExpansionAnchor.Offset.Companion {
+ method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromEnd(float offset);
+ method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromStart(float offset);
+ }
+
+ @kotlin.jvm.JvmInline public static final value class PaneExpansionAnchor.Offset.Direction {
+ field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Direction.Companion Companion;
+ }
+
+ public static final class PaneExpansionAnchor.Offset.Direction.Companion {
+ method public int getFromEnd();
+ method public int getFromStart();
+ property public final int FromEnd;
+ property public final int FromStart;
}
public static final class PaneExpansionAnchor.Proportion extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
@@ -277,8 +293,8 @@
}
public final class PaneScaffoldDirectiveKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldMotionDataProvider<Role> {
@@ -335,7 +351,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
+ public final class SupportingPaneScaffoldRole {
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getMain();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getSupporting();
@@ -360,7 +376,7 @@
method public operator androidx.compose.material3.adaptive.layout.AdaptStrategy get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldDestinationItem<T> {
+ public final class ThreePaneScaffoldDestinationItem<T> {
ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
method public T? getContentKey();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getPane();
@@ -431,7 +447,7 @@
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
+ @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
ctor public ThreePaneScaffoldValue(String primary, String secondary, String tertiary);
method public operator String get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.ignore b/compose/material3/adaptive/adaptive-layout/api/restricted_current.ignore
new file mode 100644
index 0000000..6562a08
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ParameterNameChange: androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem#ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole, T) parameter #1:
+ Attempted to change parameter name from content to contentKey in constructor androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index d075899..b4cd5c9 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -94,7 +94,7 @@
property public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
+ @kotlin.jvm.JvmInline public final value class PaneAdaptedValue {
field public static final androidx.compose.material3.adaptive.layout.PaneAdaptedValue.Companion Companion;
}
@@ -110,12 +110,28 @@
property @androidx.compose.runtime.Composable public abstract String description;
}
- public static final class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
- ctor public PaneExpansionAnchor.Offset(float offset);
- method @androidx.compose.runtime.Composable public String getDescription();
- method public float getOffset();
- property @androidx.compose.runtime.Composable public String description;
+ public abstract static class PaneExpansionAnchor.Offset extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
+ method public final int getDirection();
+ method public final float getOffset();
+ property public final int direction;
property public final float offset;
+ field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Companion Companion;
+ }
+
+ public static final class PaneExpansionAnchor.Offset.Companion {
+ method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromEnd(float offset);
+ method public androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset fromStart(float offset);
+ }
+
+ @kotlin.jvm.JvmInline public static final value class PaneExpansionAnchor.Offset.Direction {
+ field public static final androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset.Direction.Companion Companion;
+ }
+
+ public static final class PaneExpansionAnchor.Offset.Direction.Companion {
+ method public int getFromEnd();
+ method public int getFromStart();
+ property public final int FromEnd;
+ property public final int FromStart;
}
public static final class PaneExpansionAnchor.Proportion extends androidx.compose.material3.adaptive.layout.PaneExpansionAnchor {
@@ -277,8 +293,8 @@
}
public final class PaneScaffoldDirectiveKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldMotionDataProvider<Role> {
@@ -335,7 +351,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
+ public final class SupportingPaneScaffoldRole {
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getMain();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getSupporting();
@@ -360,7 +376,7 @@
method public operator androidx.compose.material3.adaptive.layout.AdaptStrategy get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldDestinationItem<T> {
+ public final class ThreePaneScaffoldDestinationItem<T> {
ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
method public T? getContentKey();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getPane();
@@ -431,7 +447,7 @@
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
+ @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
ctor public ThreePaneScaffoldValue(String primary, String secondary, String tertiary);
method public operator String get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
index 681cae4..344c0f6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
@@ -233,7 +233,7 @@
ThreePaneScaffoldRole.Secondary,
ThreePaneScaffoldRole.Tertiary
),
- PaneExpansionStateData(7, 0.8F, 9, PaneExpansionAnchor.Offset(200.dp))
+ PaneExpansionStateData(7, 0.8F, 9, PaneExpansionAnchor.Offset.fromStart(200.dp))
),
Pair(
TwoPaneExpansionStateKeyImpl(
@@ -271,12 +271,12 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockAnchor0 = PaneExpansionAnchor.Proportion(0f)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private val MockAnchor1 = PaneExpansionAnchor.Offset(200.dp)
+private val MockAnchor1 = PaneExpansionAnchor.Offset.fromStart(200.dp)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockAnchor2 = PaneExpansionAnchor.Proportion(0.5f)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private val MockAnchor3 = PaneExpansionAnchor.Offset((-200).dp)
+private val MockAnchor3 = PaneExpansionAnchor.Offset.fromEnd(200.dp)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockAnchor4 = PaneExpansionAnchor.Proportion(1f)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private val MockAnchor5 = PaneExpansionAnchor.Offset(500.dp)
+private val MockAnchor5 = PaneExpansionAnchor.Offset.fromStart(500.dp)
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index a7fd150..3677863 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -303,7 +303,7 @@
anchors =
listOf(
PaneExpansionAnchor.Proportion(0f),
- PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor)
+ PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor)
)
)
mockDraggingPx = with(LocalDensity.current) { 200.dp.toPx() }
@@ -340,7 +340,7 @@
rule.runOnIdle {
scope.launch {
mockPaneExpansionState.animateTo(
- PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor)
+ PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor)
)
}
}
@@ -368,7 +368,7 @@
rule.runOnIdle {
scope.launch {
mockPaneExpansionState.animateTo(
- PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor),
+ PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor),
200F
)
}
@@ -394,7 +394,7 @@
rule.runOnIdle {
scope.launch {
assertFailsWith<IllegalArgumentException> {
- mockPaneExpansionState.animateTo(PaneExpansionAnchor.Offset(10.dp))
+ mockPaneExpansionState.animateTo(PaneExpansionAnchor.Offset.fromStart(10.dp))
}
}
}
@@ -411,7 +411,7 @@
private val MockPaneExpansionAnchors =
listOf(
PaneExpansionAnchor.Proportion(0f),
- PaneExpansionAnchor.Offset(MockPaneExpansionMiddleAnchor),
+ PaneExpansionAnchor.Offset.fromStart(MockPaneExpansionMiddleAnchor),
PaneExpansionAnchor.Proportion(1f),
)
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt b/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt
index a563fe9..5bab1c8 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.android.kt
@@ -59,7 +59,12 @@
get() =
Strings(R.string.m3_adaptive_default_pane_expansion_proportion_anchor_description)
- actual inline val defaultPaneExpansionOffsetAnchorDescription
- get() = Strings(R.string.m3_adaptive_default_pane_expansion_offset_anchor_description)
+ actual inline val defaultPaneExpansionStartOffsetAnchorDescription
+ get() =
+ Strings(R.string.m3_adaptive_default_pane_expansion_start_offset_anchor_description)
+
+ actual inline val defaultPaneExpansionEndOffsetAnchorDescription
+ get() =
+ Strings(R.string.m3_adaptive_default_pane_expansion_end_offset_anchor_description)
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml
index d550f15..120b08a 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-as/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"পে’ন সম্প্ৰসাৰণ কৰিবলৈ টনা হেণ্ডেল"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"পে’নৰ বিভাজন %sলৈ সলনি কৰক"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d শতাংশ"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml
index 33e0970..c1e08b3 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-en-rCA/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Pane expansion drag handle"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Change pane split to %s"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d percent"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml
index d25df6b..f333cf2 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-hi/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"पैनल को बड़ा करने के लिए, खींचकर छोड़ने वाला हैंडल"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"पैनल स्प्लिट को %s में बदलें"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d प्रतिशत"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d डीपी"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml
index c287ba9..bfbceb0 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ja/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"ペインの展開のドラッグ ハンドル"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"ペインの分割を %s に変更"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d パーセント"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml
index d5cbb1b..f9f4e65 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ka/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"არეს გაფართოების სახელური ჩავლებისთვის"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"არეს გაყოფის შეცვლა %s-ით"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d პროცენტი"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml
index 0747fcb..2b63179c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ml/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"പെയിൻ വികസിപ്പിക്കാനായി വലിച്ചിടുന്നതിനുള്ള ഹാൻഡിൽ"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"പെയിൻ വിഭജനം %s ആയി മാറ്റുക"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d ശതമാനം"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP-കൾ"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml
index 81f4fb8..889db66e 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-ms/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Pemegang seret pengembangan anak tetingkap"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Tukar anak tetingkap terpisah kepada %s"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d peratus"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml
index 19a0cd0..ec85843 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pl/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Uchwyt do przeciągania panelu"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Zmień podział panelu na %s"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d procent"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DP"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml
index 2dae797..115f3c0 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values-pt-rPT/strings.xml
@@ -20,5 +20,4 @@
<string name="m3_adaptive_default_pane_expansion_drag_handle_content_description" msgid="9058489142432490820">"Indicador para arrastar de expansão do painel"</string>
<string name="m3_adaptive_default_pane_expansion_drag_handle_action_description" msgid="9031621431415014327">"Altere a divisão do painel para %s"</string>
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description" msgid="1205294531112795522">"%d por cento"</string>
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description" msgid="8189074525698747223">"%d DPs"</string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml
index 38b807f..94e3ffa 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/res/values/strings.xml
@@ -28,7 +28,14 @@
<string name="m3_adaptive_default_pane_expansion_proportion_anchor_description">
%d percent
</string>
- <!-- Spoken description of a pane expansion anchor point based on offset in DPs a user can
- anchor the pane expansion to. -->
- <string name="m3_adaptive_default_pane_expansion_offset_anchor_description">%d DPs</string>
+ <!-- Spoken description of a pane expansion anchor point based on offset from start in DPs a
+ user can anchor the pane expansion to. -->
+ <string name="m3_adaptive_default_pane_expansion_start_offset_anchor_description">
+ %d DPs from start
+ </string>
+ <!-- Spoken description of a pane expansion anchor point based on offset from end in DPs a user
+ can anchor the pane expansion to. -->
+ <string name="m3_adaptive_default_pane_expansion_end_offset_anchor_description">
+ %d DPs from end
+ </string>
</resources>
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt
index 00deed0..080903b 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneAdaptedValue.kt
@@ -16,13 +16,10 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-
/**
* The adapted state of a pane. It gives clues to pane scaffolds about if a certain pane should be
* composed and how.
*/
-@ExperimentalMaterial3AdaptiveApi
@JvmInline
value class PaneAdaptedValue private constructor(private val description: String) {
companion object {
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
index 226fc29..733dc1f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
@@ -370,13 +370,10 @@
* @param initialVelocity the initial velocity of the animation
*/
suspend fun animateTo(anchor: PaneExpansionAnchor, initialVelocity: Float = 0F) {
- require(Snapshot.withoutReadObservation { anchors.contains(anchor) }) {
- "The provided $anchor is not in the anchor list!"
- }
+ require(anchors.contains(anchor)) { "The provided $anchor is not in the anchor list!" }
currentAnchor = anchor
measuredDensity?.apply {
- val position =
- Snapshot.withoutReadObservation { anchor.positionIn(maxExpansionWidth, this) }
+ val position = anchor.positionIn(maxExpansionWidth, this)
animateToInternal(position, initialVelocity)
}
}
@@ -405,11 +402,11 @@
anchors.toPositions(
// When maxExpansionWidth is updated, the anchor positions will be
// recalculated.
- Snapshot.withoutReadObservation { maxExpansionWidth },
+ maxExpansionWidth,
it
)
}
- if (!anchors.contains(Snapshot.withoutReadObservation { currentAnchor })) {
+ if (!anchors.contains(currentAnchor)) {
currentAnchor = null
}
this.anchoringAnimationSpec = anchoringAnimationSpec
@@ -591,7 +588,10 @@
* [PaneExpansionAnchor] implementation that specifies the anchor position in the proportion of
* the total size of the layout at the start side of the anchor.
*
- * @property proportion the proportion of the layout at the start side of the anchor. layout.
+ * @param proportion the proportion of the layout at the start side of the anchor. For example,
+ * if the current layout from the start to the end is list-detail, when the proportion value
+ * is 0.3 and this anchor is used, the list pane will occupy 30% of the layout and the detail
+ * pane will occupy 70% of it.
*/
class Proportion(@FloatRange(0.0, 1.0) val proportion: Float) : PaneExpansionAnchor() {
override val type = ProportionType
@@ -619,40 +619,107 @@
}
/**
- * [PaneExpansionAnchor] implementation that specifies the anchor position in the offset in
- * [Dp]. If a positive value is provided, the offset will be treated as a start offset, on the
- * other hand, if a negative value is provided, the absolute value of the provided offset will
- * be used as an end offset. For example, if -150.dp is provided, the resulted anchor will be at
- * the position that is 150dp away from the end side of the associated layout.
+ * [PaneExpansionAnchor] implementation that specifies the anchor position based on the offset
+ * in [Dp].
*
* @property offset the offset of the anchor in [Dp].
*/
- class Offset(val offset: Dp) : PaneExpansionAnchor() {
- override val type = OffsetType
-
- override val description
- @Composable
- get() =
- getString(Strings.defaultPaneExpansionOffsetAnchorDescription, offset.value.toInt())
-
- override fun positionIn(totalSizePx: Int, density: Density) =
- with(density) { offset.roundToPx() }.let { if (it < 0) totalSizePx + it else it }
+ abstract class Offset internal constructor(val offset: Dp, override internal val type: Int) :
+ PaneExpansionAnchor() {
+ /**
+ * Indicates the direction of the offset.
+ *
+ * @see Direction.FromStart
+ * @see Direction.FromEnd
+ */
+ val direction: Direction = Direction(type)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Offset) return false
- return offset == other.offset
+ return offset == other.offset && direction == other.direction
}
override fun hashCode(): Int {
- return offset.hashCode()
+ return offset.hashCode() * 31 + direction.hashCode()
+ }
+
+ /** Represents the direction from where the offset will be calculated. */
+ @JvmInline
+ value class Direction internal constructor(internal val value: Int) {
+ companion object {
+ /**
+ * Indicates the offset will be calculated from the start. For example, if the
+ * offset is 150.dp, the resulted anchor will be at the position that is 150dp away
+ * from the start side of the associated layout.
+ */
+ val FromStart = Direction(OffsetFromStartType)
+
+ /**
+ * Indicates the offset will be calculated from the end. For example, if the offset
+ * is 150.dp, the resulted anchor will be at the position that is 150dp away from
+ * the end side of the associated layout.
+ */
+ val FromEnd = Direction(OffsetFromEndType)
+ }
+ }
+
+ private class StartOffset(offset: Dp) : Offset(offset, OffsetFromStartType) {
+ override val description
+ @Composable
+ get() =
+ getString(
+ Strings.defaultPaneExpansionStartOffsetAnchorDescription,
+ offset.value.toInt()
+ )
+
+ override fun positionIn(totalSizePx: Int, density: Density) =
+ with(density) { offset.roundToPx() }
+ }
+
+ private class EndOffset(offset: Dp) : Offset(offset, OffsetFromEndType) {
+ override val description
+ @Composable
+ get() =
+ getString(
+ Strings.defaultPaneExpansionEndOffsetAnchorDescription,
+ offset.value.toInt()
+ )
+
+ override fun positionIn(totalSizePx: Int, density: Density) =
+ totalSizePx - with(density) { offset.roundToPx() }
+ }
+
+ companion object {
+ /**
+ * Create an [androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset]
+ * anchor from the start side of the layout.
+ *
+ * @param offset offset to be used in [Dp].
+ */
+ fun fromStart(offset: Dp): Offset {
+ require(offset >= 0.dp) { "Offset must larger than or equal to 0 dp." }
+ return StartOffset(offset)
+ }
+
+ /**
+ * Create an [androidx.compose.material3.adaptive.layout.PaneExpansionAnchor.Offset]
+ * anchor from the end side of the layout.
+ *
+ * @param offset offset to be used in [Dp].
+ */
+ fun fromEnd(offset: Dp): Offset {
+ require(offset >= 0.dp) { "Offset must larger than or equal to 0 dp." }
+ return EndOffset(offset)
+ }
}
}
internal companion object {
internal const val UnspecifiedType = 0
internal const val ProportionType = 1
- internal const val OffsetType = 2
+ internal const val OffsetFromStartType = 2
+ internal const val OffsetFromEndType = 3
}
}
@@ -728,8 +795,10 @@
when (currentAnchorType) {
PaneExpansionAnchor.ProportionType ->
PaneExpansionAnchor.Proportion(it[6] as Float)
- PaneExpansionAnchor.OffsetType ->
- PaneExpansionAnchor.Offset((it[6] as Float).dp)
+ PaneExpansionAnchor.OffsetFromStartType ->
+ PaneExpansionAnchor.Offset.fromStart((it[6] as Float).dp)
+ PaneExpansionAnchor.OffsetFromEndType ->
+ PaneExpansionAnchor.Offset.fromEnd((it[6] as Float).dp)
else -> null
}
object : Map.Entry<PaneExpansionStateKey, PaneExpansionStateData> {
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
index a2a06c7..8c32cd7 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
@@ -18,7 +18,6 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.allVerticalHingeBounds
@@ -44,7 +43,6 @@
* vertical hinges.
* @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
*/
-@ExperimentalMaterial3AdaptiveApi
@Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
fun calculatePaneScaffoldDirective(
windowAdaptiveInfo: WindowAdaptiveInfo,
@@ -69,7 +67,6 @@
val maxVerticalPartitions: Int
val verticalPartitionSpacerSize: Dp
- // TODO(conradchen): Confirm the table top mode settings
if (windowAdaptiveInfo.windowPosture.isTabletop) {
maxVerticalPartitions = 2
verticalPartitionSpacerSize = 24.dp
@@ -109,7 +106,6 @@
* vertical hinges.
* @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
*/
-@ExperimentalMaterial3AdaptiveApi
@Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo: WindowAdaptiveInfo,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt
index 4ff2966..e11af66 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.kt
@@ -27,7 +27,8 @@
val defaultPaneExpansionDragHandleContentDescription: Strings
val defaultPaneExpansionDragHandleActionDescription: Strings
val defaultPaneExpansionProportionAnchorDescription: Strings
- val defaultPaneExpansionOffsetAnchorDescription: Strings
+ val defaultPaneExpansionStartOffsetAnchorDescription: Strings
+ val defaultPaneExpansionEndOffsetAnchorDescription: Strings
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
index 1fae8c8..2764c13 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
@@ -177,7 +177,6 @@
* three pane scaffolds. We suggest you to use the values defined here instead of the raw
* [ThreePaneScaffoldRole] under the context of [SupportingPaneScaffold] for better code clarity.
*/
-@ExperimentalMaterial3AdaptiveApi
object SupportingPaneScaffoldRole {
/**
* The main pane of [SupportingPaneScaffold], which is supposed to hold the major content of an
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
index 8ebafe4..fc1dfd9 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
@@ -16,8 +16,6 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-
/**
* An item representing a navigation destination in a [ThreePaneScaffold].
*
@@ -25,7 +23,6 @@
* @param contentKey the optional key or id representing the content of the destination. The type
* [T] must be storable in a Bundle.
*/
-@ExperimentalMaterial3AdaptiveApi
class ThreePaneScaffoldDestinationItem<out T>(
val pane: ThreePaneScaffoldRole,
val contentKey: T? = null,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
index d119518..71c42c2 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
@@ -22,7 +22,6 @@
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.ui.util.fastForEachReversed
-@ExperimentalMaterial3AdaptiveApi
private inline fun buildThreePaneScaffoldValue(
buildAction: (ThreePaneScaffoldRole) -> PaneAdaptedValue
): ThreePaneScaffoldValue {
@@ -168,7 +167,7 @@
* @param tertiary [PaneAdaptedValue] of the tertiary pane of [ThreePaneScaffold]
* @constructor create an instance of [ThreePaneScaffoldValue]
*/
-@ExperimentalMaterial3AdaptiveApi
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Immutable
class ThreePaneScaffoldValue(
val primary: PaneAdaptedValue,
diff --git a/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt
index a419136..11184b6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/jvmStubsMain/kotlin/androidx/compose/material3/adaptive/layout/Strings.jvmStubs.kt
@@ -39,7 +39,9 @@
implementedInJetBrainsFork()
actual val defaultPaneExpansionProportionAnchorDescription: Strings =
implementedInJetBrainsFork()
- actual val defaultPaneExpansionOffsetAnchorDescription: Strings =
+ actual val defaultPaneExpansionStartOffsetAnchorDescription: Strings =
+ implementedInJetBrainsFork()
+ actual val defaultPaneExpansionEndOffsetAnchorDescription: Strings =
implementedInJetBrainsFork()
}
}
diff --git a/compose/material3/adaptive/adaptive-navigation/api/current.txt b/compose/material3/adaptive/adaptive-navigation/api/current.txt
index 0e485ac..ba0712b 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/current.txt
@@ -6,7 +6,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
+ @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
field public static final androidx.compose.material3.adaptive.navigation.BackNavigationBehavior.Companion Companion;
}
diff --git a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
index 0e485ac..ba0712b 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
@@ -6,7 +6,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState? paneExpansionState);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
+ @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
field public static final androidx.compose.material3.adaptive.navigation.BackNavigationBehavior.Companion Companion;
}
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
index 66f554d7..37c8d7f 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
@@ -16,13 +16,11 @@
package androidx.compose.material3.adaptive.navigation
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
/** A class to control how back navigation should behave in a [ThreePaneScaffoldNavigator]. */
-@ExperimentalMaterial3AdaptiveApi
@JvmInline
value class BackNavigationBehavior private constructor(private val description: String) {
override fun toString(): String = this.description
diff --git a/compose/material3/adaptive/adaptive/api/current.txt b/compose/material3/adaptive/adaptive/api/current.txt
index 7dc7be8..a93216b 100644
--- a/compose/material3/adaptive/adaptive/api/current.txt
+++ b/compose/material3/adaptive/adaptive/api/current.txt
@@ -2,7 +2,7 @@
package androidx.compose.material3.adaptive {
public final class AndroidPosture_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
+ method public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
}
public final class AndroidWindowAdaptiveInfo_androidKt {
diff --git a/compose/material3/adaptive/adaptive/api/restricted_current.txt b/compose/material3/adaptive/adaptive/api/restricted_current.txt
index 7dc7be8..a93216b 100644
--- a/compose/material3/adaptive/adaptive/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive/api/restricted_current.txt
@@ -2,7 +2,7 @@
package androidx.compose.material3.adaptive {
public final class AndroidPosture_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
+ method public static androidx.compose.material3.adaptive.Posture calculatePosture(java.util.List<? extends androidx.window.layout.FoldingFeature> foldingFeatures);
}
public final class AndroidWindowAdaptiveInfo_androidKt {
diff --git a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
index 60b16e60..1ace339 100644
--- a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
+++ b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
@@ -23,7 +23,6 @@
* Calculates the [Posture] for a given list of [FoldingFeature]s. This methods converts framework
* folding info into the Material-opinionated posture info.
*/
-@ExperimentalMaterial3AdaptiveApi
fun calculatePosture(foldingFeatures: List<FoldingFeature>): Posture {
var isTableTop = false
val hingeList = mutableListOf<HingeInfo>()
diff --git a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
index 798e57e..5181bf1 100644
--- a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
+++ b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
@@ -438,8 +438,8 @@
private val PaneExpansionAnchors =
listOf(
PaneExpansionAnchor.Proportion(0f),
- PaneExpansionAnchor.Proportion(0.25f),
+ PaneExpansionAnchor.Offset.fromStart(360.dp),
PaneExpansionAnchor.Proportion(0.5f),
- PaneExpansionAnchor.Proportion(0.75f),
+ PaneExpansionAnchor.Offset.fromEnd(360.dp),
PaneExpansionAnchor.Proportion(1f),
)
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 97ef2d4..273f29f 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -272,25 +272,25 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ButtonGroupDefaults {
- method public float getAnimateFraction();
- method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
- method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
method public float getConnectedSpaceBetween();
- method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
- method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
- method public float getSpaceBetween();
- property public final float animateFraction;
- property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
- property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
- property public final float connectedSpaceBetween;
- property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
- property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
- property public final float spaceBetween;
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
+ method public float getExpandedRatio();
+ method public androidx.compose.foundation.layout.Arrangement.Horizontal getHorizontalArrangement();
+ property public final float ConnectedSpaceBetween;
+ property public final float ExpandedRatio;
+ property public final androidx.compose.foundation.layout.Arrangement.Horizontal HorizontalArrangement;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
field public static final androidx.compose.material3.ButtonGroupDefaults INSTANCE;
}
public final class ButtonGroupKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float animateFraction, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float expandedRatio, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public interface ButtonGroupScope {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 97ef2d4..273f29f 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -272,25 +272,25 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ButtonGroupDefaults {
- method public float getAnimateFraction();
- method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
- method public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonPressShape();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedLeadingButtonShape();
method public float getConnectedSpaceBetween();
- method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
- method public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
- method public float getSpaceBetween();
- property public final float animateFraction;
- property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
- property public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
- property public final float connectedSpaceBetween;
- property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
- property public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
- property public final float spaceBetween;
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonPressShape();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getConnectedTrailingButtonShape();
+ method public float getExpandedRatio();
+ method public androidx.compose.foundation.layout.Arrangement.Horizontal getHorizontalArrangement();
+ property public final float ConnectedSpaceBetween;
+ property public final float ExpandedRatio;
+ property public final androidx.compose.foundation.layout.Arrangement.Horizontal HorizontalArrangement;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonPressShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedLeadingButtonShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonPressShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape connectedTrailingButtonShape;
field public static final androidx.compose.material3.ButtonGroupDefaults INSTANCE;
}
public final class ButtonGroupKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float animateFraction, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ButtonGroup(optional androidx.compose.ui.Modifier modifier, optional @FloatRange(from=0.0) float expandedRatio, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ButtonGroupScope,kotlin.Unit> content);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public interface ButtonGroupScope {
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt
index cdd3981..aedab65 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ButtonGroupSamples.kt
@@ -107,8 +107,8 @@
ButtonGroup(
modifier = Modifier.padding(horizontal = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.connectedSpaceBetween),
- animateFraction = 0f
+ horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
+ expandedRatio = 0f
) {
options.forEachIndexed { index, label ->
ToggleButton(
@@ -158,8 +158,8 @@
ButtonGroup(
modifier = Modifier.padding(horizontal = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.connectedSpaceBetween),
- animateFraction = 0f
+ horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
+ expandedRatio = 0f
) {
options.forEachIndexed { index, label ->
ToggleButton(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt
index fb0b3e3..156c847 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonGroupTest.kt
@@ -101,9 +101,9 @@
@Test
fun default_firstPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.15f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * animateFraction)
+ val expandedWeight = 0.15f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * expandedWeight)
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
@@ -147,9 +147,9 @@
@Test
fun default_secondPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.15f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * (animateFraction / 2f))
+ val expandedWeight = 0.15f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * (expandedWeight / 2f))
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
@@ -193,9 +193,9 @@
@Test
fun default_thirdPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.15f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * (animateFraction / 2f))
+ val expandedWeight = 0.15f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * (expandedWeight / 2f))
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
@@ -239,9 +239,9 @@
@Test
fun default_fourthPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.15f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * animateFraction)
+ val expandedWeight = 0.15f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * expandedWeight)
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
@@ -285,13 +285,13 @@
@Test
fun customAnimateFraction_firstPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.3f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * animateFraction)
+ val expandedWeight = 0.3f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * expandedWeight)
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
- ButtonGroup(animateFraction = animateFraction) {
+ ButtonGroup(expandedRatio = expandedWeight) {
Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
Text("A")
}
@@ -331,13 +331,13 @@
@Test
fun customAnimateFraction_secondPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.3f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * animateFraction / 2f)
+ val expandedWeight = 0.3f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * expandedWeight / 2f)
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
- ButtonGroup(animateFraction = animateFraction) {
+ ButtonGroup(expandedRatio = expandedWeight) {
Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
Text("A")
}
@@ -377,13 +377,13 @@
@Test
fun customAnimateFraction_thirdPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.3f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * animateFraction / 2f)
+ val expandedWeight = 0.3f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * expandedWeight / 2f)
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
- ButtonGroup(animateFraction = animateFraction) {
+ ButtonGroup(expandedRatio = expandedWeight) {
Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
Text("A")
}
@@ -423,13 +423,13 @@
@Test
fun customAnimateFraction_fourthPressed_buttonSizing() {
val width = 75.dp
- val animateFraction = 0.3f
- val expectedExpandWidth = width + (width * animateFraction)
- val expectedCompressWidth = width - (width * animateFraction)
+ val expandedWeight = 0.3f
+ val expectedExpandWidth = width + (width * expandedWeight)
+ val expectedCompressWidth = width - (width * expandedWeight)
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.testTag(wrapperTestTag)) {
- ButtonGroup(animateFraction = animateFraction) {
+ ButtonGroup(expandedRatio = expandedWeight) {
Button(modifier = Modifier.width(width).testTag(aButton), onClick = {}) {
Text("A")
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
index ace32d4..b555741 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
@@ -47,6 +47,7 @@
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
@@ -80,9 +81,13 @@
* @sample androidx.compose.material3.samples.MultiSelectConnectedButtonGroupSample
* @sample androidx.compose.material3.samples.SingleSelectConnectedButtonGroupSample
* @param modifier the [Modifier] to be applied to the button group.
- * @param animateFraction the percentage, represented by a float, of the width of the interacted
- * child element that will be used to expand the interacted child element as well as compress the
- * neighboring children.
+ * @param expandedRatio the percentage, represented by a float, of the width of the interacted child
+ * element that will be used to expand the interacted child element as well as compress the
+ * neighboring children. By Default, standard button group will expand the interacted child
+ * element by [ButtonGroupDefaults.ExpandedRatio] of its width and this will be propagated to its
+ * neighbors. If 0f is passed into this slot, then the interacted child element will not expand at
+ * all and the neighboring elements will not compress. If 1f is passed into this slot, then the
+ * interacted child element will expand to 200% of its default width when pressed.
* @param horizontalArrangement The horizontal arrangement of the button group's children.
* @param content the content displayed in the button group, expected to use a Material3 component
* or a composable that is tagged with [Modifier.interactionSourceData].
@@ -91,9 +96,8 @@
@ExperimentalMaterial3ExpressiveApi
fun ButtonGroup(
modifier: Modifier = Modifier,
- @FloatRange(0.0) animateFraction: Float = ButtonGroupDefaults.animateFraction,
- horizontalArrangement: Arrangement.Horizontal =
- Arrangement.spacedBy(ButtonGroupDefaults.spaceBetween),
+ @FloatRange(0.0) expandedRatio: Float = ButtonGroupDefaults.ExpandedRatio,
+ horizontalArrangement: Arrangement.Horizontal = ButtonGroupDefaults.HorizontalArrangement,
content: @Composable ButtonGroupScope.() -> Unit
) {
// TODO Load the motionScheme tokens from the component tokens file
@@ -132,7 +136,7 @@
pressedIndex = index
coroutineScope.launch {
anim.animateTo(
- targetValue = animateFraction,
+ targetValue = expandedRatio,
animationSpec = defaultAnimationSpec
)
}
@@ -181,56 +185,66 @@
/**
* The default percentage, represented as a float, of the width of the interacted child element
* that will be used to expand the interacted child element as well as compress the neighboring
- * children.
+ * children. By Default, standard button group will expand the interacted child element by 15%
+ * of its width and this will be propagated to its neighbors.
*/
- val animateFraction = 0.15f
+ val ExpandedRatio = 0.15f
- /** The default spacing used between children. */
- val spaceBetween = ButtonGroupSmallTokens.BetweenSpace
+ /** The default Arrangement used between children. */
+ val HorizontalArrangement: Arrangement.Horizontal =
+ Arrangement.spacedBy(ButtonGroupSmallTokens.BetweenSpace)
/** The default spacing used between children for connected button group */
// TODO replace with token value
- val connectedSpaceBetween = 2.dp
+ val ConnectedSpaceBetween: Dp = 2.dp
/** Default shape for the leading button in a connected button group */
- val connectedLeadingButtonShape: Shape =
- // TODO replace with token value
- RoundedCornerShape(
- topStart = ShapeDefaults.CornerFull,
- bottomStart = ShapeDefaults.CornerFull,
- topEnd = ShapeDefaults.CornerSmall,
- bottomEnd = ShapeDefaults.CornerSmall
- )
+ val connectedLeadingButtonShape: Shape
+ @Composable
+ get() =
+ // TODO replace with token value
+ RoundedCornerShape(
+ topStart = ShapeDefaults.CornerFull,
+ bottomStart = ShapeDefaults.CornerFull,
+ topEnd = ShapeDefaults.CornerSmall,
+ bottomEnd = ShapeDefaults.CornerSmall
+ )
/** Default shape for the pressed state for the leading button in a connected button group. */
- val connectedLeadingButtonPressShape: Shape =
- // TODO replace with token value
- RoundedCornerShape(
- topStart = ShapeDefaults.CornerFull,
- bottomStart = ShapeDefaults.CornerFull,
- topEnd = ShapeDefaults.CornerExtraSmall,
- bottomEnd = ShapeDefaults.CornerExtraSmall
- )
+ val connectedLeadingButtonPressShape: Shape
+ @Composable
+ get() =
+ // TODO replace with token value
+ RoundedCornerShape(
+ topStart = ShapeDefaults.CornerFull,
+ bottomStart = ShapeDefaults.CornerFull,
+ topEnd = ShapeDefaults.CornerExtraSmall,
+ bottomEnd = ShapeDefaults.CornerExtraSmall
+ )
/** Default shape for the trailing button in a connected button group */
- val connectedTrailingButtonShape: Shape =
- // TODO replace with token value
- RoundedCornerShape(
- topEnd = ShapeDefaults.CornerFull,
- bottomEnd = ShapeDefaults.CornerFull,
- topStart = ShapeDefaults.CornerSmall,
- bottomStart = ShapeDefaults.CornerSmall
- )
+ val connectedTrailingButtonShape: Shape
+ @Composable
+ get() =
+ // TODO replace with token value
+ RoundedCornerShape(
+ topEnd = ShapeDefaults.CornerFull,
+ bottomEnd = ShapeDefaults.CornerFull,
+ topStart = ShapeDefaults.CornerSmall,
+ bottomStart = ShapeDefaults.CornerSmall
+ )
/** Default shape for the pressed state for the trailing button in a connected button group. */
- val connectedTrailingButtonPressShape: Shape =
- // TODO replace with token value
- RoundedCornerShape(
- topEnd = ShapeDefaults.CornerFull,
- bottomEnd = ShapeDefaults.CornerFull,
- topStart = ShapeDefaults.CornerExtraSmall,
- bottomStart = ShapeDefaults.CornerExtraSmall
- )
+ val connectedTrailingButtonPressShape: Shape
+ @Composable
+ get() =
+ // TODO replace with token value
+ RoundedCornerShape(
+ topEnd = ShapeDefaults.CornerFull,
+ bottomEnd = ShapeDefaults.CornerFull,
+ topStart = ShapeDefaults.CornerExtraSmall,
+ bottomStart = ShapeDefaults.CornerExtraSmall
+ )
}
private class ButtonGroupMeasurePolicy(
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5a884ad..2b88ca8 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -228,7 +228,6 @@
public abstract class AutofillManager {
method public abstract void cancel();
method public abstract void commit();
- method public abstract void requestAutofillForActiveElement();
}
public final class AutofillNode {
@@ -2860,6 +2859,7 @@
public final class DelegatableNodeKt {
method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c0d712..8b114f0 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -228,7 +228,6 @@
public abstract class AutofillManager {
method public abstract void cancel();
method public abstract void commit();
- method public abstract void requestAutofillForActiveElement();
}
public final class AutofillNode {
@@ -2914,6 +2913,7 @@
public final class DelegatableNodeKt {
method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index d6ce34a..25dcd50 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -42,11 +42,14 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.elementOf
+import androidx.compose.ui.node.requestAutofill
import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.contentDataType
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.contentType
@@ -713,68 +716,6 @@
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = 26)
- fun autofillManager_requestAutofillAfterFocus() {
- val am: PlatformAutofillManager = mock()
- val contextMenuTag = "menu_tag"
- var autofillManager: AutofillManager?
-
- rule.setContent {
- autofillManager = LocalAutofillManager.current
- (autofillManager as AndroidAutofillManager).platformAutofillManager = am
- Box(
- modifier =
- Modifier.semantics {
- testTag = contextMenuTag
- onAutofillText { true }
- }
- .focusProperties { canFocus = true }
- .clickable { autofillManager?.requestAutofillForActiveElement() }
- .size(height, width)
- )
- }
-
- // `requestAutofill` is always called after an element is focused
- rule.onNodeWithTag(contextMenuTag).requestFocus()
- rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
-
- // then `requestAutofill` is called on that same previously focused element
- rule.onNodeWithTag(contextMenuTag).performClick()
- rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
- }
-
- @Test
- @SmallTest
- @SdkSuppress(minSdkVersion = 26)
- fun autofillManager_notAutofillable_doesNotrequestAutofillAfterFocus() {
- val am: PlatformAutofillManager = mock()
- val contextMenuTag = "menu_tag"
- var autofillManager: AutofillManager?
-
- rule.setContent {
- autofillManager = LocalAutofillManager.current
- (autofillManager as AndroidAutofillManager).platformAutofillManager = am
- Box(
- modifier =
- Modifier.semantics { testTag = contextMenuTag }
- .focusProperties { canFocus = true }
- .clickable { autofillManager?.requestAutofillForActiveElement() }
- .size(height, width)
- )
- }
- clearInvocations(am)
-
- // `requestAutofill` is always called after an element is focused
- rule.onNodeWithTag(contextMenuTag).requestFocus()
- rule.runOnIdle { verifyZeroInteractions(am) }
-
- // then `requestAutofill` is called on that same previously focused element
- rule.onNodeWithTag(contextMenuTag).performClick()
- rule.runOnIdle { verifyNoMoreInteractions(am) }
- }
-
- @Test
- @SmallTest
fun autofillManager_lazyColumnScroll_callsCommit() {
lateinit var state: LazyListState
lateinit var coroutineScope: CoroutineScope
@@ -866,4 +807,35 @@
// A column disappearing will call commit
rule.runOnIdle { verify(am).commit() }
}
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofillManager_requestAutofill() {
+ val am: PlatformAutofillManager = mock()
+ val semanticsModifier = TestSemanticsModifier { testTag = "TestTag" }
+ var autofillManager: AutofillManager?
+
+ rule.setContent {
+ autofillManager = LocalAutofillManager.current
+ (autofillManager as AndroidAutofillManager).platformAutofillManager = am
+ Box(Modifier.elementOf(semanticsModifier))
+ }
+
+ // Act
+ rule.runOnIdle { semanticsModifier.requestAutofill() }
+
+ // Assert
+ rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
+ }
+
+ private class TestSemanticsModifier(
+ private val onApplySemantics: SemanticsPropertyReceiver.() -> Unit
+ ) : SemanticsModifierNode, Modifier.Node() {
+
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ contentType = ContentType.Username
+ onApplySemantics.invoke(this)
+ }
+ }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 84314e3e..1d90f0b 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -3510,6 +3510,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) {}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 3f9150a..ffb3fec 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -2846,6 +2846,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override val rootForTest: RootForTest
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 76aa7ce..4322a1c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -281,6 +281,10 @@
override fun requestFocus() = TODO("Not yet implemented")
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun onSemanticsChange() {}
override fun getFocusDirection(keyEvent: KeyEvent) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index a36713d..5d5e649 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -521,6 +521,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) {}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
index 7cfa2d4c..c49374e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsModifierNodeTest.kt
@@ -77,9 +77,7 @@
// Assert.
rule.runOnIdle {
if (precomputedSemantics) {
- // One invocation when the modifier node calls autoInvalidateNodeSelf and another
- // when the Layout node is attached.
- assertThat(semanticsModifier.applySemanticsInvocations).isEqualTo(2)
+ assertThat(semanticsModifier.applySemanticsInvocations).isEqualTo(1)
} else {
assertThat(semanticsModifier.applySemanticsInvocations).isEqualTo(0)
}
@@ -194,7 +192,7 @@
// Assert.
rule.runOnIdle {
assertThat(semanticsModifier.applySemanticsInvocations)
- .isEqualTo(if (precomputedSemantics) 8 else 2)
+ .isEqualTo(if (precomputedSemantics) 7 else 2)
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 8e4a41b..4c213f7 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -26,12 +26,16 @@
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.ReusableContent
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentDataType
import androidx.compose.ui.autofill.ContentType
@@ -134,6 +138,40 @@
.assert(SemanticsMatcher.expectValue(SemanticsProperties.PaneTitle, paneTitleString))
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun testSemanticsCalculatedOncePerComposition() {
+ var recomposeScope: RecomposeScope? = null
+ var count = 0
+ fun Modifier.count() = semantics { count++ }
+ rule.setContent {
+ recomposeScope = currentRecomposeScope
+ Box(
+ modifier = Modifier.count().count().count(),
+ )
+ }
+ rule.runOnIdle {
+ if (ComposeUiFlags.isSemanticAutofillEnabled) {
+ // with autofill on, semantics is eagerly evaluated
+ assertThat(count).isEqualTo(3)
+ } else {
+ // before autofill, semantics was lazily evaluated
+ assertThat(count).isEqualTo(0)
+ }
+ count = 0
+ recomposeScope!!.invalidate()
+ }
+ rule.runOnIdle {
+ if (ComposeUiFlags.isSemanticAutofillEnabled) {
+ // with autofill on, semantics is eagerly evaluated
+ assertThat(count).isEqualTo(3)
+ } else {
+ // before autofill, semantics was lazily evaluated
+ assertThat(count).isEqualTo(0)
+ }
+ }
+ }
+
@Test
@Suppress("DEPRECATION")
fun isContainerProperty_unmergedConfig() {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 69f31cc..4d49938 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -77,19 +77,6 @@
platformAutofillManager.cancel()
}
- // This will be used to request autofill when
- // `AutofillManager.requestAutofillForActiveElement()` is called (e.g. from the text toolbar).
- private var previouslyFocusedId = -1
-
- override fun requestAutofillForActiveElement() {
- if (previouslyFocusedId <= 0) return
-
- rectManager.rects.withRect(previouslyFocusedId) { left, top, right, bottom ->
- reusableRect.set(left, top, right, bottom)
- platformAutofillManager.requestAutofill(view, previouslyFocusedId, reusableRect)
- }
- }
-
override fun onFocusChanged(
previous: FocusTargetModifierNode?,
current: FocusTargetModifierNode?
@@ -105,7 +92,6 @@
rectManager.rects.withRect(semanticsId) { l, t, r, b ->
platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
}
- previouslyFocusedId = semanticsId
}
}
}
@@ -138,7 +124,6 @@
val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
val currFocus = config?.getOrNull(SemanticsProperties.Focused)
if (previousFocus != true && currFocus == true && config.isAutofillable()) {
- previouslyFocusedId = semanticsId
rectManager.rects.withRect(semanticsId) { l, t, r, b ->
platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
}
@@ -236,6 +221,13 @@
private var currentlyDisplayedIDs = MutableIntSet()
private var pendingChangesToDisplayedIds = false
+ internal fun requestAutofill(semanticsInfo: SemanticsInfo) {
+ rectManager.rects.withRect(semanticsInfo.semanticsId) { left, top, right, bottom ->
+ reusableRect.set(left, top, right, bottom)
+ platformAutofillManager.requestAutofill(view, semanticsInfo.semanticsId, reusableRect)
+ }
+ }
+
internal fun onPostAttach(semanticsInfo: SemanticsInfo) {
if (semanticsInfo.semanticsConfiguration?.isRelatedToAutoCommit() == true) {
currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index c059f5d..7da948c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -1247,6 +1247,13 @@
}
}
+ override fun requestAutofill(node: LayoutNode) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.requestAutofill(node)
+ }
+ }
+
fun requestClearInvalidObservations() {
observationClearRequested = true
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index 0328f65..25e747f 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -776,7 +776,8 @@
check(owner is MockOwner)
owner.onRequestMeasureParams.clear()
owner.invalidatedLayers.clear()
- owner.semanticsChanged = false
+ layoutNode.isSemanticsInvalidated = false
+ // owner.semanticsChanged = false
}
private fun NodeChain.layoutInvalidated(): Boolean {
@@ -792,9 +793,7 @@
}
private fun NodeChain.semanticsInvalidated(): Boolean {
- val owner = layoutNode.owner
- check(owner is MockOwner)
- return owner.semanticsChanged
+ return layoutNode.isSemanticsInvalidated
}
internal fun layout(
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index f13b1f1..faa645f 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -2448,6 +2448,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) {}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
@@ -2538,11 +2542,7 @@
}
}
- var semanticsChanged: Boolean = false
-
- override fun onSemanticsChange() {
- semanticsChanged = true
- }
+ override fun onSemanticsChange() {}
override fun onLayoutChange(layoutNode: LayoutNode) {
layoutChangeCount++
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 4fdd4ea..de1b8d6 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -446,6 +446,10 @@
override fun requestFocus() = TODO("Not yet implemented")
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) = TODO("Not yet implemented")
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
index 2a6163b..f5ebdc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
@@ -42,14 +42,4 @@
* without processing any information entered in the autofillable field.
*/
abstract fun cancel()
-
- /**
- * Request autofill for previously focused element.
- *
- * This may have no effect, and it is not required that any autofill service will be notified.
- *
- * Any component that can be autofilled may call this when it is active to request an autofill
- * services response.
- */
- abstract fun requestAutofillForActiveElement()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index cba5ff1..d60c1b5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -46,7 +46,7 @@
fun Modifier.onPreviewKeyEvent(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier =
this then KeyInputElement(onKeyEvent = null, onPreKeyEvent = onPreviewKeyEvent)
-private data class KeyInputElement(
+private class KeyInputElement(
val onKeyEvent: ((KeyEvent) -> Boolean)?,
val onPreKeyEvent: ((KeyEvent) -> Boolean)?
) : ModifierNodeElement<KeyInputNode>() {
@@ -67,6 +67,21 @@
properties["onPreviewKeyEvent"] = it
}
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is KeyInputElement) return false
+
+ if (onKeyEvent !== other.onKeyEvent) return false
+ if (onPreKeyEvent !== other.onPreKeyEvent) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = onKeyEvent?.hashCode() ?: 0
+ result = 31 * result + (onPreKeyEvent?.hashCode() ?: 0)
+ return result
+ }
}
private class KeyInputNode(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 08505aa..8b45c3f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -332,6 +332,13 @@
checkPreconditionNotNull(requireLayoutNode().owner) { "This node does not have an owner." }
/**
+ * Requests autofill for the LayoutNode that this [DelegatableNode] is attached to. If the node does
+ * not have any autofill semantic properties set, then the request still may be sent to the Autofill
+ * service, but no response is expected.
+ */
+fun DelegatableNode.requestAutofill() = requireLayoutNode().requestAutofill()
+
+/**
* Returns the current [Density] of the LayoutNode that this [DelegatableNode] is attached to. If
* the node is not attached, this function will throw an [IllegalStateException].
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 5f60779..ef5efb6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -400,7 +400,15 @@
override fun isTransparent(): Boolean = outerCoordinator.isTransparent()
- private var isSemanticsInvalidated = false
+ internal var isSemanticsInvalidated = false
+
+ internal fun requestAutofill() {
+ // Ignore calls while semantics are being applied (b/378114177).
+ if (isCurrentlyCalculatingSemanticsConfiguration) return
+
+ val owner = requireOwner()
+ owner.requestAutofill(this)
+ }
internal fun invalidateSemantics() {
// Ignore calls to invalidate Semantics while semantics are being applied (b/378114177).
@@ -923,6 +931,9 @@
requirePrecondition(!isDeactivated) { "modifier is updated when deactivated" }
if (isAttached) {
applyModifier(value)
+ if (isSemanticsInvalidated) {
+ invalidateSemantics()
+ }
} else {
pendingModifier = value
}
@@ -935,19 +946,6 @@
if (lookaheadRoot == null && nodes.has(Nodes.ApproachMeasure)) {
lookaheadRoot = this
}
- // Notify semantics listeners if semantics was invalidated.
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isSemanticAutofillEnabled && isSemanticsInvalidated) {
- val prev = _semanticsConfiguration
- _semanticsConfiguration = calculateSemanticsConfiguration()
- isSemanticsInvalidated = false
- val owner = requireOwner()
- owner.semanticsOwner.notifySemanticsChange(this, prev)
-
- // This is needed for Accessibility and ContentCapture. Remove after these systems
- // are migrated to use SemanticsInfo and SemanticListeners.
- owner.onSemanticsChange()
- }
}
private fun resetModifierState() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index a4f5fe6..eb27c9a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -331,7 +331,7 @@
node.invalidateDraw()
}
if (Nodes.Semantics in selfKindSet && node is SemanticsModifierNode) {
- node.invalidateSemantics()
+ node.requireLayoutNode().isSemanticsInvalidated = true
}
if (Nodes.ParentData in selfKindSet && node is ParentDataModifierNode) {
node.invalidateParentData()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index a75bf87..ee7342a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -245,6 +245,9 @@
*/
fun requestFocus(): Boolean
+ /** Ask the system to request autofill values to this owner. */
+ fun requestAutofill(node: LayoutNode)
+
/**
* Iterates through all LayoutNodes that have requested layout and measures and lays them out.
* If [sendPointerUpdate] is `true` then a simulated PointerEvent may be sent to update pointer
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
index 43db9f6..d8d5f55 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
@@ -125,6 +125,7 @@
* index than startIndex, and at the end of the Carousel, we will not populate the slots that have a
* higher index than startIndex.
*
+ * @param motionScene the MotionScene which holds slots and can be used to customize the carousel
* @param initialSlotIndex the slot index that holds the current element
* @param numSlots the number of slots in the scene
* @param backwardTransition the name of the previous transition (default "previous")
@@ -137,7 +138,7 @@
@Composable
@Suppress("UnavailableSymbol")
fun MotionCarousel(
- @Suppress("HiddenTypeParameter") motionScene: MotionScene,
+ motionScene: MotionScene,
initialSlotIndex: Int,
numSlots: Int,
backwardTransition: String = "backward",
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java
index 3575eb4..fef3c9c 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/ArrayLinkedVariables.java
@@ -230,8 +230,10 @@
* The code is broadly identical to the put() method, only differing
* in in-line deletion, and of course doing an add rather than a put
*
- * @param variable the variable we want to add
- * @param value its value
+ * @param variable the variable we want to add
+ * @param value its value
+ * @param removeFromDefinition if a variable's value becomes zero, it is removed. This parameter
+ * is whether the removal is deep now or left shallow until cleanup
*/
@Override
public void add(SolverVariable variable, float value, boolean removeFromDefinition) {
@@ -355,7 +357,9 @@
/**
* Update the current list with a new definition
*
- * @param definition the row containing the definition
+ * @param definition the row containing the definition
+ * @param removeFromDefinition if a variable's value becomes zero, it is removed. This parameter
+ * is whether the removal is deep now or left shallow until cleanup
*/
@Override
public float use(ArrayRow definition, boolean removeFromDefinition) {
@@ -374,7 +378,9 @@
/**
* Remove a variable from the list
*
- * @param variable the variable we want to remove
+ * @param variable the variable we want to remove
+ * @param removeFromDefinition if a variable's value becomes zero, it is removed. This parameter
+ * is whether the removal is deep now or left shallow until cleanup
* @return the value of the removed variable
*/
@Override
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java
index 5706151..e4e1c7af 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/LinearSystem.java
@@ -1509,6 +1509,8 @@
* Add the equations constraining a widget center to another widget center, positioned
* on a circle, following an angle and radius
*
+ * @param widget the constrained widget
+ * @param target the constrained-to widget
* @param angle from 0 to 360
* @param radius the distance between the two centers
*/
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
index f2dc0ee..ba98915 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/Motion.java
@@ -280,7 +280,8 @@
* x coordinate of "time" 0.0 mPoints[point.length-1] is filled with the y coordinate of "time"
* 1.0
*
- * @param points array to fill (should be 2x the number of mPoints
+ * @param points array to fill (should be 2x pointCount)
+ * @param pointCount truncate mPoints to this length. Must be > 1 and <= mPoints.length
*/
public void buildPath(float[] points, int pointCount) {
float mils = 1.0f / (pointCount - 1);
@@ -1646,6 +1647,7 @@
* ...
* length
*
+ * @param type if type is -1, skip all keyframes with type != -1
* @param info is a data structure array of int that holds info on each keyframe
* @return Number of keyFrames found
*/
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java
index 943b41d..b3c5443 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/KeyCycleOscillator.java
@@ -139,9 +139,12 @@
* sets a oscillator wave point
*
* @param framePosition the position
+ * @param shape the wave shape
+ * @param waveString the wave string
* @param variesBy only varies by path supported for now
* @param period the period of the wave
* @param offset the offset value
+ * @param phase the phase of the new wavepoint
* @param value the adder
* @param custom The ConstraintAttribute used to set the value
*/
@@ -167,9 +170,12 @@
* sets a oscillator wave point
*
* @param framePosition the position
+ * @param shape the wave shape
+ * @param waveString the wave string
* @param variesBy only varies by path supported for now
* @param period the period of the wave
* @param offset the offset value
+ * @param phase the phase of the new wavepoint
* @param value the adder
*/
public void setPoint(int framePosition,
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java
index 5b7d71d..76b5128 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/CoreMotionScene.java
@@ -25,6 +25,7 @@
* set the Transitions string onto the MotionScene
*
* @param elementName the name of the element
+ * @param toJSON the json string of the transitioncontent
*/
void setTransitionContent(String elementName, String toJSON);
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java
index 98dbf1f..cf66b93 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/Transition.java
@@ -432,10 +432,11 @@
* Converts from xy drag to progress
* This should be used till touch up
*
- * @param baseW parent width
- * @param baseH parent height
- * @param dx change in x
- * @param dy change in y
+ * @param currentProgress 0...1 progress in
+ * @param baseW parent width
+ * @param baseH parent height
+ * @param dx change in x
+ * @param dy change in y
* @return the change in progress
*/
public float dragToProgress(float currentProgress, int baseW, int baseH, float dx, float dy) {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java
index e06973f..a60a9d3 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/Chain.java
@@ -41,6 +41,8 @@
*
* @param constraintWidgetContainer root container
* @param system the linear system we add the equations to
+ * @param widgets if this is null or contains any chainheads' widgets,
+ * constrain all chains / the corresponding chains
* @param orientation HORIZONTAL or VERTICAL
*/
public static void applyChainConstraints(
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java
index 6644ed9d..af42a35 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidgetContainer.java
@@ -451,6 +451,7 @@
* Update the frame of the layout and its children from the solver
*
* @param system the solver we get the values from.
+ * @param flags the flag set associated with this solver
*/
public boolean updateChildrenFromSolver(LinearSystem system, boolean[] flags) {
flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = false;
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java
index ef67572..8f96e8f 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/DesignTool.java
@@ -184,6 +184,7 @@
*
* @param view view to getMap the animation of
* @param path array to be filled (x1,y1,x2,y2...)
+ * @param len the desired number of point along animation
* @return -1 if not under and animation 0 if not animated or number of point along animation
*/
public int getAnimationPath(Object view, float[] path, int len) {
@@ -446,7 +447,8 @@
* The call is designed to be efficient because it will be called 30x Number of views a second
*
* @param view the view to return keyframe positions
- * @param info
+ * @param type if type is -1, skip all keyframes with type != -1
+ * @param info array to fill with info on each keyframe
* @return Number of keyFrames found
*/
public int getKeyFrameInfo(Object view, int type, int[] info) {
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
index 4dc61c2..a1cdd45 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionController.java
@@ -1675,6 +1675,7 @@
* ...
* length
*
+ * @param type if type is -1, skip all keyframes with type != -1
* @param info is a data structure array of int that holds info on each keyframe
* @return Number of keyFrames found
*/
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
index f633da5a..b45ca83 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
@@ -3659,7 +3659,8 @@
* Elevation logic is based on style and animation. By default it is not used because it would
* lead to unexpected results.
*
- * @param apply true if this constraint set applies elevation to this view
+ * @param viewId ID of view to adjust the elevation
+ * @param apply true if this constraint set applies elevation to this view
*/
public void setApplyElevation(int viewId, boolean apply) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
@@ -4200,9 +4201,10 @@
/**
* Creates a ConstraintLayout Barrier object.
*
- * @param id
+ * @param id the id of the constraint to create or partially overwrite.
* @param direction Barrier.{LEFT,RIGHT,TOP,BOTTOM,START,END}
- * @param referenced
+ * @param margin the barrierMargin of the Barrier object
+ * @param referenced the referenceIds of the Barrier object
*/
public void createBarrier(int id, int direction, int margin, int... referenced) {
Constraint constraint = get(id);
diff --git a/libraryversions.toml b/libraryversions.toml
index 3985367..59c4949 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -168,10 +168,10 @@
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha02"
-WEAR_PHONE_INTERACTIONS = "1.1.0-alpha05"
-WEAR_PROTOLAYOUT = "1.3.0-alpha06"
+WEAR_PHONE_INTERACTIONS = "1.1.0-beta01"
+WEAR_PROTOLAYOUT = "1.3.0-alpha07"
WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
-WEAR_TILES = "1.5.0-alpha06"
+WEAR_TILES = "1.5.0-alpha07"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
WEAR_WATCHFACE = "1.3.0-alpha05"
WEBKIT = "1.13.0-alpha03"
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
index b6bdb70..927612c 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/current.txt
@@ -1,10 +1,10 @@
// Signature format: 4.0
package androidx.lifecycle.viewmodel.navigation3 {
- public final class ViewModelStoreNavContentWrapper implements androidx.navigation3.NavContentWrapper {
- method @androidx.compose.runtime.Composable public void WrapBackStack(java.util.List<?> backStack);
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
- field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper INSTANCE;
+ public final class ViewModelStoreNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+ method @androidx.compose.runtime.Composable public void ProvideToBackStack(java.util.List<?> backStack);
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+ field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavLocalProvider INSTANCE;
}
}
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
index b6bdb70..927612c 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/api/restricted_current.txt
@@ -1,10 +1,10 @@
// Signature format: 4.0
package androidx.lifecycle.viewmodel.navigation3 {
- public final class ViewModelStoreNavContentWrapper implements androidx.navigation3.NavContentWrapper {
- method @androidx.compose.runtime.Composable public void WrapBackStack(java.util.List<?> backStack);
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
- field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper INSTANCE;
+ public final class ViewModelStoreNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+ method @androidx.compose.runtime.Composable public void ProvideToBackStack(java.util.List<?> backStack);
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+ field public static final androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavLocalProvider INSTANCE;
}
}
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
index f0efd6c..f59f4fa 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapperTest.kt
@@ -26,9 +26,8 @@
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.NavDisplay
-import androidx.navigation3.NavRecord
-import androidx.navigation3.SavedStateNavContentWrapper
-import androidx.navigation3.rememberNavWrapperManager
+import androidx.navigation3.NavEntry
+import androidx.navigation3.SavedStateNavLocalProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import kotlin.test.Test
@@ -37,65 +36,65 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-class ViewModelStoreNavContentWrapperTest {
+class ViewModelStoreNavLocalProviderTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun testViewModelProvided() {
- val savedStateWrapper = SavedStateNavContentWrapper
- val viewModelWrapper = ViewModelStoreNavContentWrapper
+ val savedStateWrapper = SavedStateNavLocalProvider
+ val viewModelWrapper = ViewModelStoreNavLocalProvider
lateinit var viewModel1: MyViewModel
lateinit var viewModel2: MyViewModel
- val record1Arg = "record1 Arg"
- val record2Arg = "record2 Arg"
- val record1 =
- NavRecord("key1") {
+ val entry1Arg = "entry1 Arg"
+ val entry2Arg = "entry2 Arg"
+ val entry1 =
+ NavEntry("key1") {
viewModel1 = viewModel<MyViewModel>()
- viewModel1.myArg = record1Arg
+ viewModel1.myArg = entry1Arg
}
- val record2 =
- NavRecord("key2") {
+ val entry2 =
+ NavEntry("key2") {
viewModel2 = viewModel<MyViewModel>()
- viewModel2.myArg = record2Arg
+ viewModel2.myArg = entry2Arg
}
composeTestRule.setContent {
- savedStateWrapper.WrapContent(
- NavRecord(record1.key) { viewModelWrapper.WrapContent(record1) }
+ savedStateWrapper.ProvideToEntry(
+ NavEntry(entry1.key) { viewModelWrapper.ProvideToEntry(entry1) }
)
- savedStateWrapper.WrapContent(
- NavRecord(record2.key) { viewModelWrapper.WrapContent(record2) }
+ savedStateWrapper.ProvideToEntry(
+ NavEntry(entry2.key) { viewModelWrapper.ProvideToEntry(entry2) }
)
}
composeTestRule.runOnIdle {
- assertWithMessage("Incorrect arg for record 1")
+ assertWithMessage("Incorrect arg for entry 1")
.that(viewModel1.myArg)
- .isEqualTo(record1Arg)
- assertWithMessage("Incorrect arg for record 2")
+ .isEqualTo(entry1Arg)
+ assertWithMessage("Incorrect arg for entry 2")
.that(viewModel2.myArg)
- .isEqualTo(record2Arg)
+ .isEqualTo(entry2Arg)
}
}
@Test
- fun testViewModelNoSavedStateNavContentWrapper() {
- val viewModelWrapper = ViewModelStoreNavContentWrapper
+ fun testViewModelNoSavedStateNavLocalProvider() {
+ val viewModelWrapper = ViewModelStoreNavLocalProvider
lateinit var viewModel1: MyViewModel
- val record1Arg = "record1 Arg"
- val record1 =
- NavRecord("key1") {
+ val entry1Arg = "entry1 Arg"
+ val entry1 =
+ NavEntry("key1") {
viewModel1 = viewModel<MyViewModel>()
- viewModel1.myArg = record1Arg
+ viewModel1.myArg = entry1Arg
}
try {
- composeTestRule.setContent { viewModelWrapper.WrapContent(record1) }
+ composeTestRule.setContent { viewModelWrapper.ProvideToEntry(entry1) }
} catch (e: Exception) {
assertThat(e)
.hasMessageThat()
.isEqualTo(
"The Lifecycle state is already beyond INITIALIZED. The " +
- "ViewModelStoreNavContentWrapper requires adding the " +
- "SavedStateNavContentWrapper to ensure support for " +
+ "ViewModelStoreNavLocalProvider requires adding the " +
+ "SavedStateNavLocalProvider to ensure support for " +
"SavedStateHandles."
)
}
@@ -106,21 +105,17 @@
lateinit var backStack: MutableList<Any>
composeTestRule.setContent {
backStack = remember { mutableStateListOf("Home") }
- val manager =
- rememberNavWrapperManager(
- listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
- )
NavDisplay(
backstack = backStack,
- wrapperManager = manager,
+ localProviders = listOf(SavedStateNavLocalProvider, ViewModelStoreNavLocalProvider),
onBack = { backStack.removeAt(backStack.lastIndex) },
) { key ->
when (key) {
"Home" -> {
- NavRecord(key) { viewModel<HomeViewModel>() }
+ NavEntry(key) { viewModel<HomeViewModel>() }
}
"AnotherScreen" -> {
- NavRecord(key) { viewModel<HomeViewModel>() }
+ NavEntry(key) { viewModel<HomeViewModel>() }
}
else -> error("Unknown key: $key")
}
@@ -160,18 +155,14 @@
lateinit var viewModel: SavedStateViewModel
composeTestRule.setContent {
backStack = remember { mutableStateListOf("Home") }
- val manager =
- rememberNavWrapperManager(
- listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
- )
NavDisplay(
backstack = backStack,
- wrapperManager = manager,
+ localProviders = listOf(SavedStateNavLocalProvider, ViewModelStoreNavLocalProvider),
onBack = { backStack.removeAt(backStack.lastIndex) },
) { key ->
when (key) {
"Home" -> {
- NavRecord(key) {
+ NavEntry(key) {
viewModel =
viewModel<SavedStateViewModel> {
val handle = createSavedStateHandle()
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt b/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
index 79402711..37b32ac 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
+++ b/lifecycle/lifecycle-viewmodel-navigation3/src/androidMain/kotlin/androidx/lifecycle/viewmodel/navigation3/ViewModelStoreNavContentWrapper.android.kt
@@ -35,44 +35,44 @@
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation3.NavContentWrapper
-import androidx.navigation3.NavRecord
+import androidx.navigation3.NavEntry
+import androidx.navigation3.NavLocalProvider
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
/**
- * Provides the content of a [NavRecord] with a [ViewModelStoreOwner] and provides that
+ * Provides the content of a [NavEntry] with a [ViewModelStoreOwner] and provides that
* [ViewModelStoreOwner] as a [LocalViewModelStoreOwner] so that it is available within the content.
*
- * This requires that usage of the [SavedStateNavContentWrapper] to ensure that the [NavRecord]
- * scoped [ViewModel]s can properly provide access to [SavedStateHandle]s
+ * This requires that usage of the [SavedStateNavLocalProvider] to ensure that the [NavEntry] scoped
+ * [ViewModel]s can properly provide access to [SavedStateHandle]s
*/
-public object ViewModelStoreNavContentWrapper : NavContentWrapper {
+public object ViewModelStoreNavLocalProvider : NavLocalProvider {
@Composable
- override fun WrapBackStack(backStack: List<Any>) {
- val recordViewModelStoreProvider = viewModel { RecordViewModel() }
- recordViewModelStoreProvider.ownerInBackStack.clear()
- recordViewModelStoreProvider.ownerInBackStack.addAll(backStack)
+ override fun ProvideToBackStack(backStack: List<Any>) {
+ val entryViewModelStoreProvider = viewModel { EntryViewModel() }
+ entryViewModelStoreProvider.ownerInBackStack.clear()
+ entryViewModelStoreProvider.ownerInBackStack.addAll(backStack)
}
@Composable
- override fun <T : Any> WrapContent(record: NavRecord<T>) {
- val key = record.key
- val recordViewModelStoreProvider = viewModel { RecordViewModel() }
- val viewModelStore = recordViewModelStoreProvider.viewModelStoreForKey(key)
+ override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
+ val key = entry.key
+ val entryViewModelStoreProvider = viewModel { EntryViewModel() }
+ val viewModelStore = entryViewModelStoreProvider.viewModelStoreForKey(key)
// This ensures we always keep viewModels on config changes.
val activity = LocalActivity.current
remember(key, viewModelStore) {
object : RememberObserver {
override fun onAbandoned() {
- if (!recordViewModelStoreProvider.ownerInBackStack.contains(key)) {
+ if (!entryViewModelStoreProvider.ownerInBackStack.contains(key)) {
disposeIfNotChangingConfiguration()
}
}
override fun onForgotten() {
- if (!recordViewModelStoreProvider.ownerInBackStack.contains(key)) {
+ if (!entryViewModelStoreProvider.ownerInBackStack.contains(key)) {
disposeIfNotChangingConfiguration()
}
}
@@ -81,7 +81,7 @@
fun disposeIfNotChangingConfiguration() {
if (activity?.isChangingConfigurations != true) {
- recordViewModelStoreProvider.removeViewModelStoreOwnerForKey(key)?.clear()
+ entryViewModelStoreProvider.removeViewModelStoreOwnerForKey(key)?.clear()
}
}
}
@@ -110,20 +110,20 @@
init {
require(this.lifecycle.currentState == Lifecycle.State.INITIALIZED) {
"The Lifecycle state is already beyond INITIALIZED. The " +
- "ViewModelStoreNavContentWrapper requires adding the " +
- "SavedStateNavContentWrapper to ensure support for " +
+ "ViewModelStoreNavLocalProvider requires adding the " +
+ "SavedStateNavLocalProvider to ensure support for " +
"SavedStateHandles."
}
enableSavedStateHandles()
}
}
) {
- record.content.invoke(key)
+ entry.content.invoke(key)
}
}
}
-private class RecordViewModel : ViewModel() {
+private class EntryViewModel : ViewModel() {
private val owners = mutableMapOf<Any, ViewModelStore>()
val ownerInBackStack = mutableListOf<Any>()
diff --git a/navigation3/navigation3/api/current.txt b/navigation3/navigation3/api/current.txt
index ec7e433..d8d07e0 100644
--- a/navigation3/navigation3/api/current.txt
+++ b/navigation3/navigation3/api/current.txt
@@ -1,9 +1,48 @@
// Signature format: 4.0
package androidx.navigation3 {
- public interface NavContentWrapper {
- method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
+ public final class EntryClassProvider<T> {
+ ctor public EntryClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.reflect.KClass<T> component1();
+ method public java.util.Map<java.lang.String,java.lang.Object> component2();
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+ method public androidx.navigation3.EntryClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.reflect.KClass<T> getClazz();
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+ method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+ property public final kotlin.reflect.KClass<T> clazz;
+ property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+ property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ }
+
+ @kotlin.DslMarker public @interface EntryDsl {
+ }
+
+ public final class EntryProvider<T> {
+ ctor public EntryProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public T component1();
+ method public java.util.Map<java.lang.String,java.lang.Object> component2();
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+ method public androidx.navigation3.EntryProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+ method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+ method public T getKey();
+ property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+ property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ property public final T key;
+ }
+
+ @androidx.navigation3.EntryDsl public final class EntryProviderBuilder {
+ ctor public EntryProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback);
+ method public <T> void addEntryProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public <T> void addEntryProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> build();
+ }
+
+ public final class EntryProviderKt {
+ method public static inline <reified T> void entry(androidx.navigation3.EntryProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public static <T> void entry(androidx.navigation3.EntryProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> entryProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.EntryProviderBuilder,kotlin.Unit> builder);
}
public final class NavDisplay {
@@ -13,11 +52,11 @@
}
public final class NavDisplay_androidKt {
- method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+ method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional java.util.List<? extends androidx.navigation3.NavLocalProvider> localProviders, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavEntry<? extends T>> entryProvider);
}
- public final class NavRecord<T> {
- ctor public NavRecord(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ public final class NavEntry<T> {
+ ctor public NavEntry(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
method public T getKey();
@@ -26,69 +65,30 @@
property public final T key;
}
+ public interface NavLocalProvider {
+ method @androidx.compose.runtime.Composable public default void ProvideToBackStack(java.util.List<?> backStack);
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+ }
+
public final class NavWrapperManager {
ctor public NavWrapperManager();
- ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
- method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
+ ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
+ method @androidx.compose.runtime.Composable public <T> void ContentForEntry(androidx.navigation3.NavEntry<T> entry);
method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
}
public final class NavWrapperManagerKt {
- method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+ method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
}
- public final class RecordClassProvider<T> {
- ctor public RecordClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.reflect.KClass<T> component1();
- method public java.util.Map<java.lang.String,java.lang.Object> component2();
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
- method public androidx.navigation3.RecordClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.reflect.KClass<T> getClazz();
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
- method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
- property public final kotlin.reflect.KClass<T> clazz;
- property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
- property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ public final class SaveableStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+ ctor public SaveableStateNavLocalProvider();
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
}
- @kotlin.DslMarker public @interface RecordDsl {
- }
-
- public final class RecordProvider<T> {
- ctor public RecordProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public T component1();
- method public java.util.Map<java.lang.String,java.lang.Object> component2();
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
- method public androidx.navigation3.RecordProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
- method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
- method public T getKey();
- property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
- property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
- property public final T key;
- }
-
- @androidx.navigation3.RecordDsl public final class RecordProviderBuilder {
- ctor public RecordProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback);
- method public <T> void addRecordProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public <T> void addRecordProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> build();
- }
-
- public final class RecordProviderKt {
- method public static inline <reified T> void record(androidx.navigation3.RecordProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public static <T> void record(androidx.navigation3.RecordProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> recordProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.RecordProviderBuilder,kotlin.Unit> builder);
- }
-
- public final class SaveableStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
- ctor public SaveableStateNavContentWrapper();
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
- }
-
- public final class SavedStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
- field public static final androidx.navigation3.SavedStateNavContentWrapper INSTANCE;
+ public final class SavedStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+ field public static final androidx.navigation3.SavedStateNavLocalProvider INSTANCE;
}
}
diff --git a/navigation3/navigation3/api/restricted_current.txt b/navigation3/navigation3/api/restricted_current.txt
index ec7e433..d8d07e0 100644
--- a/navigation3/navigation3/api/restricted_current.txt
+++ b/navigation3/navigation3/api/restricted_current.txt
@@ -1,9 +1,48 @@
// Signature format: 4.0
package androidx.navigation3 {
- public interface NavContentWrapper {
- method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
+ public final class EntryClassProvider<T> {
+ ctor public EntryClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.reflect.KClass<T> component1();
+ method public java.util.Map<java.lang.String,java.lang.Object> component2();
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+ method public androidx.navigation3.EntryClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.reflect.KClass<T> getClazz();
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+ method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+ property public final kotlin.reflect.KClass<T> clazz;
+ property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+ property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ }
+
+ @kotlin.DslMarker public @interface EntryDsl {
+ }
+
+ public final class EntryProvider<T> {
+ ctor public EntryProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public T component1();
+ method public java.util.Map<java.lang.String,java.lang.Object> component2();
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
+ method public androidx.navigation3.EntryProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
+ method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
+ method public T getKey();
+ property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
+ property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ property public final T key;
+ }
+
+ @androidx.navigation3.EntryDsl public final class EntryProviderBuilder {
+ ctor public EntryProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback);
+ method public <T> void addEntryProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public <T> void addEntryProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> build();
+ }
+
+ public final class EntryProviderKt {
+ method public static inline <reified T> void entry(androidx.navigation3.EntryProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public static <T> void entry(androidx.navigation3.EntryProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavEntry<? extends java.lang.Object?>> entryProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavEntry<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.EntryProviderBuilder,kotlin.Unit> builder);
}
public final class NavDisplay {
@@ -13,11 +52,11 @@
}
public final class NavDisplay_androidKt {
- method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+ method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional java.util.List<? extends androidx.navigation3.NavLocalProvider> localProviders, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavEntry<? extends T>> entryProvider);
}
- public final class NavRecord<T> {
- ctor public NavRecord(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
+ public final class NavEntry<T> {
+ ctor public NavEntry(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
method public T getKey();
@@ -26,69 +65,30 @@
property public final T key;
}
+ public interface NavLocalProvider {
+ method @androidx.compose.runtime.Composable public default void ProvideToBackStack(java.util.List<?> backStack);
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+ }
+
public final class NavWrapperManager {
ctor public NavWrapperManager();
- ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
- method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
+ ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
+ method @androidx.compose.runtime.Composable public <T> void ContentForEntry(androidx.navigation3.NavEntry<T> entry);
method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
}
public final class NavWrapperManagerKt {
- method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+ method @androidx.compose.runtime.Composable public static androidx.navigation3.NavWrapperManager rememberNavWrapperManager(java.util.List<? extends androidx.navigation3.NavLocalProvider> navLocalProviders);
}
- public final class RecordClassProvider<T> {
- ctor public RecordClassProvider(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.reflect.KClass<T> component1();
- method public java.util.Map<java.lang.String,java.lang.Object> component2();
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
- method public androidx.navigation3.RecordClassProvider<T> copy(kotlin.reflect.KClass<T> clazz, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.reflect.KClass<T> getClazz();
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
- method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
- property public final kotlin.reflect.KClass<T> clazz;
- property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
- property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
+ public final class SaveableStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+ ctor public SaveableStateNavLocalProvider();
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
}
- @kotlin.DslMarker public @interface RecordDsl {
- }
-
- public final class RecordProvider<T> {
- ctor public RecordProvider(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public T component1();
- method public java.util.Map<java.lang.String,java.lang.Object> component2();
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> component3();
- method public androidx.navigation3.RecordProvider<T> copy(T key, java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.jvm.functions.Function1<T,kotlin.Unit> getContent();
- method public java.util.Map<java.lang.String,java.lang.Object> getFeatureMap();
- method public T getKey();
- property public final kotlin.jvm.functions.Function1<T,kotlin.Unit> content;
- property public final java.util.Map<java.lang.String,java.lang.Object> featureMap;
- property public final T key;
- }
-
- @androidx.navigation3.RecordDsl public final class RecordProviderBuilder {
- ctor public RecordProviderBuilder(kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback);
- method public <T> void addRecordProvider(kotlin.reflect.KClass<T> clazz, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public <T> void addRecordProvider(T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> build();
- }
-
- public final class RecordProviderKt {
- method public static inline <reified T> void record(androidx.navigation3.RecordProviderBuilder, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public static <T> void record(androidx.navigation3.RecordProviderBuilder, T key, optional java.util.Map<java.lang.String,?> featureMap, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> content);
- method public static inline kotlin.jvm.functions.Function1<java.lang.Object,androidx.navigation3.NavRecord<? extends java.lang.Object?>> recordProvider(optional kotlin.jvm.functions.Function1<java.lang.Object,? extends androidx.navigation3.NavRecord<? extends java.lang.Object?>> fallback, kotlin.jvm.functions.Function1<? super androidx.navigation3.RecordProviderBuilder,kotlin.Unit> builder);
- }
-
- public final class SaveableStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
- ctor public SaveableStateNavContentWrapper();
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
- }
-
- public final class SavedStateNavContentWrapper implements androidx.navigation3.NavContentWrapper {
- method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
- field public static final androidx.navigation3.SavedStateNavContentWrapper INSTANCE;
+ public final class SavedStateNavLocalProvider implements androidx.navigation3.NavLocalProvider {
+ method @androidx.compose.runtime.Composable public <T> void ProvideToEntry(androidx.navigation3.NavEntry<T> entry);
+ field public static final androidx.navigation3.SavedStateNavLocalProvider INSTANCE;
}
}
diff --git a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
index fa6ff47..d4728e8 100644
--- a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
+++ b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
@@ -23,13 +23,12 @@
import androidx.compose.runtime.Composable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper
+import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavLocalProvider
import androidx.navigation3.NavDisplay
-import androidx.navigation3.NavRecord
-import androidx.navigation3.SavedStateNavContentWrapper
-import androidx.navigation3.record
-import androidx.navigation3.recordProvider
-import androidx.navigation3.rememberNavWrapperManager
+import androidx.navigation3.NavEntry
+import androidx.navigation3.SavedStateNavLocalProvider
+import androidx.navigation3.entry
+import androidx.navigation3.entryProvider
class ProfileViewModel : ViewModel() {
val name = "no user"
@@ -39,31 +38,27 @@
@Composable
fun BaseNav() {
val backStack = rememberMutableStateListOf(Profile)
- val manager =
- rememberNavWrapperManager(
- listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
- )
NavDisplay(
backstack = backStack,
- wrapperManager = manager,
+ localProviders = listOf(SavedStateNavLocalProvider, ViewModelStoreNavLocalProvider),
onBack = { backStack.removeLast() },
- recordProvider =
- recordProvider({ NavRecord(Unit) { Text(text = "Invalid Key") } }) {
- record<Profile>(
+ entryProvider =
+ entryProvider({ NavEntry(Unit) { Text(text = "Invalid Key") } }) {
+ entry<Profile>(
NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
) {
val viewModel = viewModel<ProfileViewModel>()
Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
}
- record<Scrollable>(
+ entry<Scrollable>(
NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
) {
Scrollable({ backStack.add(it) }) { backStack.removeLast() }
}
- record<Dialog>(featureMap = NavDisplay.isDialog(true)) {
+ entry<Dialog>(featureMap = NavDisplay.isDialog(true)) {
DialogContent { backStack.removeLast() }
}
- record<Dashboard>(
+ entry<Dashboard>(
NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
) { dashboardArgs ->
val userId = dashboardArgs.userId
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
index d7da926..6fb5955 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
@@ -49,8 +49,8 @@
backstack = remember { mutableStateListOf(first) }
NavDisplay(backstack) {
when (it) {
- first -> NavRecord(first) { Text(first) }
- second -> NavRecord(second) { Text(second) }
+ first -> NavEntry(first) { Text(first) }
+ second -> NavEntry(second) { Text(second) }
else -> error("Invalid key passed")
}
}
@@ -93,13 +93,13 @@
NavDisplay(backstack) {
when (it) {
first ->
- NavRecord(
+ NavEntry(
first,
) {
Text(first)
}
second ->
- NavRecord(
+ NavEntry(
second,
featureMap =
NavDisplay.transition(
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
index fd5749a..614eb88 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
@@ -34,6 +34,7 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertWithMessage
import kotlin.test.Test
+import kotlin.test.assertFailsWith
import org.junit.Rule
import org.junit.runner.RunWith
@@ -45,7 +46,7 @@
@Test
fun testContentShown() {
composeTestRule.setContent {
- NavDisplay(backstack = mutableStateListOf(first)) { NavRecord(first) { Text(first) } }
+ NavDisplay(backstack = mutableStateListOf(first)) { NavEntry(first) { Text(first) } }
}
assertThat(composeTestRule.onNodeWithText(first).isDisplayed()).isTrue()
@@ -58,8 +59,8 @@
backstack = remember { mutableStateListOf(first) }
NavDisplay(backstack = backstack) {
when (it) {
- first -> NavRecord(first) { Text(first) }
- second -> NavRecord(second) { Text(second) }
+ first -> NavEntry(first) { Text(first) }
+ second -> NavEntry(second) { Text(second) }
else -> error("Invalid key passed")
}
}
@@ -80,8 +81,8 @@
backstack = remember { mutableStateListOf(first) }
NavDisplay(backstack = backstack) {
when (it) {
- first -> NavRecord(first) { Text(first) }
- second -> NavRecord(second, NavDisplay.isDialog(true)) { Text(second) }
+ first -> NavEntry(first) { Text(first) }
+ second -> NavEntry(second, NavDisplay.isDialog(true)) { Text(second) }
else -> error("Invalid key passed")
}
}
@@ -105,8 +106,8 @@
backstack = remember { mutableStateListOf(first) }
NavDisplay(backstack = backstack) {
when (it) {
- first -> NavRecord(first) { Text(first) }
- second -> NavRecord(second) { Text(second) }
+ first -> NavEntry(first) { Text(first) }
+ second -> NavEntry(second) { Text(second) }
else -> error("Invalid key passed")
}
}
@@ -132,8 +133,8 @@
backstack = remember { mutableStateListOf(first) }
NavDisplay(backstack = backstack) {
when (it) {
- first -> NavRecord(first) { numberOnScreen1 = rememberSaveable { increment++ } }
- second -> NavRecord(second) {}
+ first -> NavEntry(first) { numberOnScreen1 = rememberSaveable { increment++ } }
+ second -> NavEntry(second) {}
else -> error("Invalid key passed")
}
}
@@ -142,14 +143,11 @@
composeTestRule.runOnIdle {
assertWithMessage("Initial number should be 0").that(numberOnScreen1).isEqualTo(0)
numberOnScreen1 = -1
+ assertWithMessage("The number should be -1").that(numberOnScreen1).isEqualTo(-1)
backstack.add(second)
}
- composeTestRule.runOnIdle {
- assertWithMessage("The number should be -1").that(numberOnScreen1).isEqualTo(-1)
- // removeLast requires API 35
- backstack.removeAt(backstack.size - 1)
- }
+ composeTestRule.runOnIdle { backstack.removeAt(backstack.size - 1) }
composeTestRule.runOnIdle {
assertWithMessage("The number should be restored").that(numberOnScreen1).isEqualTo(0)
@@ -165,15 +163,14 @@
composeTestRule.setContent {
mainRegistry = LocalSavedStateRegistryOwner.current.savedStateRegistry
backstack = remember { mutableStateListOf(first) }
- val manager = rememberNavWrapperManager(listOf(SavedStateNavContentWrapper))
- NavDisplay(backstack = backstack, wrapperManager = manager) {
+ NavDisplay(backstack = backstack, localProviders = listOf(SavedStateNavLocalProvider)) {
when (it) {
first ->
- NavRecord(first) {
+ NavEntry(first) {
registry1 = LocalSavedStateRegistryOwner.current.savedStateRegistry
}
second ->
- NavRecord(second) {
+ NavEntry(second) {
registry2 = LocalSavedStateRegistryOwner.current.savedStateRegistry
}
else -> error("Invalid key passed")
@@ -212,12 +209,12 @@
2 -> backStack2
else -> backStack3
},
- recordProvider =
- recordProvider {
- record(first) { Text(first) }
- record(second) { Text(second) }
- record(third) { Text(third) }
- record(forth) { Text(forth) }
+ entryProvider =
+ entryProvider {
+ entry(first) { Text(first) }
+ entry(second) { Text(second) }
+ entry(third) { Text(third) }
+ entry(forth) { Text(forth) }
}
)
}
@@ -238,6 +235,43 @@
assertThat(backStack3).containsExactly(third, forth)
assertThat(composeTestRule.onNodeWithText(forth).isDisplayed()).isTrue()
}
+
+ @Test
+ fun testInitEmptyBackstackThrows() {
+ lateinit var backstack: MutableList<Any>
+ val fail =
+ assertFailsWith<IllegalArgumentException> {
+ composeTestRule.setContent {
+ backstack = remember { mutableStateListOf() }
+ NavDisplay(backstack = backstack) { NavEntry(first) {} }
+ }
+ }
+ assertThat(fail.message).isEqualTo("NavDisplay backstack cannot be empty")
+ }
+
+ @Test
+ fun testPopToEmptyBackstackThrows() {
+ lateinit var backstack: MutableList<Any>
+ composeTestRule.setContent {
+ backstack = remember { mutableStateListOf(first) }
+ NavDisplay(backstack = backstack) {
+ when (it) {
+ first -> NavEntry(first) { Text(first) }
+ second -> NavEntry(second) { Text(second) }
+ else -> error("Invalid key passed")
+ }
+ }
+ }
+
+ assertThat(composeTestRule.onNodeWithText(first).isDisplayed()).isTrue()
+
+ val fail =
+ assertFailsWith<IllegalArgumentException> {
+ composeTestRule.runOnIdle { backstack.clear() }
+ composeTestRule.waitForIdle()
+ }
+ assertThat(fail.message).isEqualTo("NavDisplay backstack cannot be empty")
+ }
}
private const val first = "first"
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt
index be41543..646df38 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavWrapperManagerTest.kt
@@ -35,14 +35,14 @@
var calledWrapBackStack = false
var calledWrapContent = false
val wrapper =
- object : NavContentWrapper {
+ object : NavLocalProvider {
@Composable
- override fun WrapBackStack(backStack: List<Any>) {
+ override fun ProvideToBackStack(backStack: List<Any>) {
calledWrapBackStack = true
}
@Composable
- override fun <T : Any> WrapContent(record: NavRecord<T>) {
+ override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
calledWrapContent = true
}
}
@@ -51,7 +51,7 @@
composeTestRule.setContent {
manager.PrepareBackStack(listOf("something"))
- manager.ContentForRecord(NavRecord("myKey") {})
+ manager.ContentForEntry(NavEntry("myKey") {})
}
assertThat(calledWrapBackStack).isTrue()
@@ -63,14 +63,14 @@
var calledWrapBackStackCount = 0
var calledWrapContentCount = 0
val wrapper =
- object : NavContentWrapper {
+ object : NavLocalProvider {
@Composable
- override fun WrapBackStack(backStack: List<Any>) {
+ override fun ProvideToBackStack(backStack: List<Any>) {
calledWrapBackStackCount++
}
@Composable
- override fun <T : Any> WrapContent(record: NavRecord<T>) {
+ override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
calledWrapContentCount++
}
}
@@ -79,7 +79,7 @@
composeTestRule.setContent {
manager.PrepareBackStack(listOf("something"))
- manager.ContentForRecord(NavRecord("myKey") {})
+ manager.ContentForEntry(NavEntry("myKey") {})
}
assertThat(calledWrapBackStackCount).isEqualTo(1)
diff --git a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
index cdcff77..fdc1e24 100644
--- a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
+++ b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
@@ -34,7 +34,7 @@
/** Object that indicates the features that can be handled by the [NavDisplay] */
public object NavDisplay {
/**
- * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
+ * Function to be called on the [NavEntry.featureMap] to notify the [NavDisplay] that the
* content should be animated using the provided transitions.
*/
public fun transition(enter: EnterTransition?, exit: ExitTransition?): Map<String, Any> =
@@ -42,7 +42,7 @@
else mapOf(ENTER_TRANSITION_KEY to enter, EXIT_TRANSITION_KEY to exit)
/**
- * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
+ * Function to be called on the [NavEntry.featureMap] to notify the [NavDisplay] that the
* content should be displayed inside of a [Dialog]
*/
public fun isDialog(boolean: Boolean): Map<String, Any> =
@@ -60,29 +60,28 @@
*
* The NavDisplay displays the content associated with the last key on the back stack in most
* circumstances. If that content wants to be displayed as a dialog, as communicated by adding
- * [NavDisplay.isDialog] to a [NavRecord.featureMap], then the last key's content is a dialog and
- * the second to last key is a displayed in the background.
+ * [NavDisplay.isDialog] to a [NavEntry.featureMap], then the last key's content is a dialog and the
+ * second to last key is a displayed in the background.
*
* @param backstack the collection of keys that represents the state that needs to be handled
- * @param wrapperManager the manager that combines all of the [NavContentWrapper]s
+ * @param localProviders list of [NavLocalProvider] to add information to the provided entriess
* @param modifier the modifier to be applied to the layout.
* @param contentAlignment The [Alignment] of the [AnimatedContent]
- * * @param enterTransition Default [EnterTransition] for all [NavRecord]s. Can be overridden
- * * individually for each [NavRecord] by passing in the record's transitions through
- * * [NavRecord.featureMap].
- * * @param exitTransition Default [ExitTransition] for all [NavRecord]s. Can be overridden
- * * individually for each [NavRecord] by passing in the record's transitions through
- * * [NavRecord.featureMap].
- *
+ * @param enterTransition Default [EnterTransition] for all [NavEntry]s. Can be overridden
+ * individually for each [NavEntry] by passing in the entry's transitions through
+ * [NavEntry.featureMap].
+ * @param exitTransition Default [ExitTransition] for all [NavEntry]s. Can be overridden
+ * individually for each [NavEntry] by passing in the entry's transitions through
+ * [NavEntry.featureMap].
* @param onBack a callback for handling system back presses
- * @param recordProvider lambda used to construct each possible [NavRecord]
+ * @param entryProvider lambda used to construct each possible [NavEntry]
* @sample androidx.navigation3.samples.BaseNav
*/
@Composable
public fun <T : Any> NavDisplay(
backstack: List<T>,
modifier: Modifier = Modifier,
- wrapperManager: NavWrapperManager = rememberNavWrapperManager(emptyList()),
+ localProviders: List<NavLocalProvider> = emptyList(),
contentAlignment: Alignment = Alignment.TopStart,
sizeTransform: SizeTransform? = null,
enterTransition: EnterTransition =
@@ -100,33 +99,36 @@
)
),
onBack: () -> Unit = { if (backstack is MutableList) backstack.removeAt(backstack.size - 1) },
- recordProvider: (key: T) -> NavRecord<out T>
+ entryProvider: (key: T) -> NavEntry<out T>
) {
+ require(backstack.isNotEmpty()) { "NavDisplay backstack cannot be empty" }
+
+ val wrapperManager: NavWrapperManager = rememberNavWrapperManager(localProviders)
BackHandler(backstack.size > 1, onBack)
wrapperManager.PrepareBackStack(backStack = backstack)
val key = backstack.last()
- val record = recordProvider.invoke(key)
+ val entry = entryProvider.invoke(key)
- // Incoming record defines transitions, otherwise it uses default transitions from NavDisplay
+ // Incoming entry defines transitions, otherwise it uses default transitions from NavDisplay
val finalEnterTransition =
- record.featureMap[NavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition ?: enterTransition
+ entry.featureMap[NavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition ?: enterTransition
val finalExitTransition =
- record.featureMap[NavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition ?: exitTransition
+ entry.featureMap[NavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition ?: exitTransition
- val isDialog = record.featureMap[NavDisplay.DIALOG_KEY] == true
+ val isDialog = entry.featureMap[NavDisplay.DIALOG_KEY] == true
// if there is a dialog, we should create a transition with the next to last entry instead.
val transition =
if (isDialog) {
if (backstack.size > 1) {
val previousKey = backstack[backstack.size - 2]
- val previousRecord = recordProvider.invoke(previousKey)
- updateTransition(targetState = previousRecord, label = previousKey.toString())
+ val previousEntry = entryProvider.invoke(previousKey)
+ updateTransition(targetState = previousEntry, label = previousKey.toString())
} else {
null
}
} else {
- updateTransition(targetState = record, label = key.toString())
+ updateTransition(targetState = entry, label = key.toString())
}
transition?.AnimatedContent(
@@ -140,11 +142,11 @@
},
contentAlignment = contentAlignment,
contentKey = { it.key }
- ) { innerRecord ->
- wrapperManager.ContentForRecord(innerRecord)
+ ) { innerEntry ->
+ wrapperManager.ContentForEntry(innerEntry)
}
if (isDialog) {
- Dialog(onBack) { wrapperManager.ContentForRecord(record) }
+ Dialog(onBack) { wrapperManager.ContentForEntry(entry) }
}
}
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/EntryProvider.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/EntryProvider.kt
new file mode 100644
index 0000000..47471e4
--- /dev/null
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/EntryProvider.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 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.navigation3
+
+import androidx.compose.runtime.Composable
+import kotlin.reflect.KClass
+
+@DslMarker public annotation class EntryDsl
+
+/** Creates an [EntryProviderBuilder] with the entry providers provided in the builder. */
+public inline fun entryProvider(
+ noinline fallback: (unknownScreen: Any) -> NavEntry<*> = {
+ throw IllegalStateException("Unknown screen $it")
+ },
+ builder: EntryProviderBuilder.() -> Unit
+): (Any) -> NavEntry<*> = EntryProviderBuilder(fallback).apply(builder).build()
+
+/** DSL for constructing a new [NavEntry] */
+@Suppress("TopLevelBuilder")
+@EntryDsl
+public class EntryProviderBuilder(private val fallback: (unknownScreen: Any) -> NavEntry<*>) {
+ private val clazzProviders = mutableMapOf<KClass<*>, EntryClassProvider<*>>()
+ private val providers = mutableMapOf<Any, EntryProvider<*>>()
+
+ /** Builds a [NavEntry] for the given [key] that displays [content]. */
+ @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
+ public fun <T : Any> addEntryProvider(
+ key: T,
+ featureMap: Map<String, Any> = emptyMap(),
+ content: @Composable (T) -> Unit,
+ ) {
+ require(key !in providers) {
+ "An `entry` with the key `key` has already been added: ${key}."
+ }
+ providers[key] = EntryProvider(key, featureMap, content)
+ }
+
+ /** Builds a [NavEntry] for the given [clazz] that displays [content]. */
+ @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
+ public fun <T : Any> addEntryProvider(
+ clazz: KClass<T>,
+ featureMap: Map<String, Any> = emptyMap(),
+ content: @Composable (T) -> Unit,
+ ) {
+ require(clazz !in clazzProviders) {
+ "An `entry` with the same `clazz` has already been added: ${clazz.simpleName}."
+ }
+ clazzProviders[clazz] = EntryClassProvider(clazz, featureMap, content)
+ }
+
+ /**
+ * Returns an instance of entryProvider created from the entry providers set on this builder.
+ */
+ @Suppress("UNCHECKED_CAST")
+ public fun build(): (Any) -> NavEntry<*> = { key ->
+ val entryClassProvider = clazzProviders[key::class] as? EntryClassProvider<Any>
+ val entryProvider = providers[key] as? EntryProvider<Any>
+ entryClassProvider?.run { NavEntry(key, featureMap, content) }
+ ?: entryProvider?.run { NavEntry(key, featureMap, content) }
+ ?: fallback.invoke(key)
+ }
+}
+
+/** Add an entry provider to the [EntryProviderBuilder] */
+public fun <T : Any> EntryProviderBuilder.entry(
+ key: T,
+ featureMap: Map<String, Any> = emptyMap(),
+ content: @Composable (T) -> Unit,
+) {
+ addEntryProvider(key, featureMap, content)
+}
+
+/** Add an entry provider to the [EntryProviderBuilder] */
+public inline fun <reified T : Any> EntryProviderBuilder.entry(
+ featureMap: Map<String, Any> = emptyMap(),
+ noinline content: @Composable (T) -> Unit,
+) {
+ addEntryProvider(T::class, featureMap, content)
+}
+
+/** Holds a Entry class, featureMap, and content for that class */
+public data class EntryClassProvider<T : Any>(
+ val clazz: KClass<T>,
+ val featureMap: Map<String, Any>,
+ val content: @Composable (T) -> Unit,
+)
+
+/** Holds a Entry class, featureMap, and content for that key */
+public data class EntryProvider<T : Any>(
+ val key: T,
+ val featureMap: Map<String, Any>,
+ val content: @Composable (T) -> Unit,
+)
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavRecord.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavEntry.kt
similarity index 72%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavRecord.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavEntry.kt
index 71cd4e0..e5d19d9 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavRecord.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavEntry.kt
@@ -19,14 +19,14 @@
import androidx.compose.runtime.Composable
/**
- * Record maintains the store the key and the content represented by that key. Records should be
- * created as part of a [NavDisplay.recordProvider](reference/androidx/navigation/NavDisplay).
+ * Entry maintains and stores the key and the content represented by that key. Entries should be
+ * created as part of a [NavDisplay.entryProvider](reference/androidx/navigation/NavDisplay).
*
- * @param key key for this record
+ * @param key key for this entry
* @param featureMap map of the available features from a display
- * @param content content for this record to be displayed when this record is active
+ * @param content content for this entry to be displayed when this entry is active
*/
-public class NavRecord<T : Any>(
+public class NavEntry<T : Any>(
public val key: T,
public val featureMap: Map<String, Any> = emptyMap(),
public val content: @Composable (T) -> Unit,
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavLocalProvider.kt
similarity index 73%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavContentWrapper.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavLocalProvider.kt
index 42d1652..b3fbeb2 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavLocalProvider.kt
@@ -22,23 +22,23 @@
* Interface that offers the ability to provide information to some Composable content that is
* integrated with a [NavDisplay](reference/androidx/navigation/NavDisplay).
*
- * Information can be provided to the entire back stack via [NavContentWrapper.WrapBackStack] or to
- * a single record via [NavContentWrapper.WrapContent].
+ * Information can be provided to the entire back stack via [NavLocalProvider.ProvideToBackStack] or
+ * to a single entry via [NavLocalProvider.ProvideToEntry].
*/
-public interface NavContentWrapper {
+public interface NavLocalProvider {
/**
- * Allows a [NavContentWrapper] to execute on the entire backstack.
+ * Allows a [NavLocalProvider] to provide to the entire backstack.
*
* This function is called by the [NavWrapperManager] and should not be called directly.
*/
- @Composable public fun WrapBackStack(backStack: List<Any>): Unit = Unit
+ @Composable public fun ProvideToBackStack(backStack: List<Any>): Unit = Unit
/**
- * Allows a [NavContentWrapper] to provide information to the content of a single record.
+ * Allows a [NavLocalProvider] to provide information to a single entry.
*
* This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and
* should not be called directly.
*/
- @Composable public fun <T : Any> WrapContent(record: NavRecord<T>)
+ @Composable public fun <T : Any> ProvideToEntry(entry: NavEntry<T>)
}
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
index 4a137df..a994b16 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
@@ -19,56 +19,54 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-/** Creates a [NavContentWrapper]. */
+/** Creates a [NavLocalProvider]. */
@Composable
-public fun rememberNavWrapperManager(
- navContentWrappers: List<NavContentWrapper>
-): NavWrapperManager {
- return remember { NavWrapperManager(navContentWrappers) }
+public fun rememberNavWrapperManager(navLocalProviders: List<NavLocalProvider>): NavWrapperManager {
+ return remember { NavWrapperManager(navLocalProviders) }
}
/**
- * Class that manages all of the provided [NavContentWrapper]. It is responsible for executing the
- * functions provided by each [NavContentWrapper] appropriately.
+ * Class that manages all of the provided [NavLocalProvider]. It is responsible for executing the
+ * functions provided by each [NavLocalProvider] appropriately.
*
- * Note: the order in which the [NavContentWrapper]s are added to the list determines their scope,
- * i.e. a [NavContentWrapper] added earlier in a list has its data available to those added later.
+ * Note: the order in which the [NavLocalProvider]s are added to the list determines their scope,
+ * i.e. a [NavLocalProvider] added earlier in a list has its data available to those added later.
*
- * @param navContentWrappers the [NavContentWrapper]s that are providing data to the content
+ * @param navLocalProviders the [NavLocalProvider]s that are providing data to the content
*/
-public class NavWrapperManager(navContentWrappers: List<NavContentWrapper> = emptyList()) {
+public class NavWrapperManager(navLocalProviders: List<NavLocalProvider> = emptyList()) {
/**
- * Final list of wrappers. This always adds a [SaveableStateNavContentWrapper] by default, as it
+ * Final list of wrappers. This always adds a [SaveableStateNavLocalProvider] by default, as it
* is required. It then filters out any duplicates to ensure there is always one instance of any
* wrapper at a given time.
*/
private val finalWrappers =
- (navContentWrappers + listOf(SaveableStateNavContentWrapper())).distinct()
+ (navLocalProviders + listOf(SaveableStateNavLocalProvider())).distinct()
/**
- * Calls the [NavContentWrapper.WrapBackStack] functions on each wrapper
+ * Calls the [NavLocalProvider.ProvideToBackStack] functions on each wrapper
*
* This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and
* should not be called directly.
*/
@Composable
public fun PrepareBackStack(backStack: List<Any>) {
- finalWrappers.distinct().forEach { it.WrapBackStack(backStack = backStack) }
+ finalWrappers.distinct().forEach { it.ProvideToBackStack(backStack = backStack) }
}
/**
- * Calls the [NavContentWrapper.WrapContent] functions on each wrapper.
+ * Calls the [NavLocalProvider.ProvideToEntry] functions on each wrapper.
*
* This function is called by the [NavDisplay](reference/androidx/navigation/NavDisplay) and
* should not be called directly.
*/
@Composable
- public fun <T : Any> ContentForRecord(record: NavRecord<T>) {
- val key = record.key
+ public fun <T : Any> ContentForEntry(entry: NavEntry<T>) {
+ val key = entry.key
finalWrappers
.distinct()
- .foldRight(record.content) { wrapper, contentLambda ->
- { wrapper.WrapContent(NavRecord(key, record.featureMap, content = contentLambda)) }
+ .foldRight(entry.content) { wrapper, contentLambda ->
+ { wrapper.ProvideToEntry(NavEntry(key, entry.featureMap, content = contentLambda)) }
}
.invoke(key)
}
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/RecordProvider.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/RecordProvider.kt
deleted file mode 100644
index 7f6dd14..0000000
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/RecordProvider.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright 2024 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.navigation3
-
-import androidx.compose.runtime.Composable
-import kotlin.reflect.KClass
-
-@DslMarker public annotation class RecordDsl
-
-/** Creates an [RecordProviderBuilder] with the record providers provided in the builder. */
-public inline fun recordProvider(
- noinline fallback: (unknownScreen: Any) -> NavRecord<*> = {
- throw IllegalStateException("Unknown screen $it")
- },
- builder: RecordProviderBuilder.() -> Unit
-): (Any) -> NavRecord<*> = RecordProviderBuilder(fallback).apply(builder).build()
-
-/** DSL for constructing a new [NavRecord] */
-@Suppress("TopLevelBuilder")
-@RecordDsl
-public class RecordProviderBuilder(private val fallback: (unknownScreen: Any) -> NavRecord<*>) {
- private val clazzProviders = mutableMapOf<KClass<*>, RecordClassProvider<*>>()
- private val providers = mutableMapOf<Any, RecordProvider<*>>()
-
- /** Builds a [NavRecord] for the given [key] that displays [content]. */
- @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
- public fun <T : Any> addRecordProvider(
- key: T,
- featureMap: Map<String, Any> = emptyMap(),
- content: @Composable (T) -> Unit,
- ) {
- require(key !in providers) {
- "A `record` with the key `key` has already been added: ${key}."
- }
- providers[key] = RecordProvider(key, featureMap, content)
- }
-
- /** Builds a [NavRecord] for the given [clazz] that displays [content]. */
- @Suppress("SetterReturnsThis", "MissingGetterMatchingBuilder")
- public fun <T : Any> addRecordProvider(
- clazz: KClass<T>,
- featureMap: Map<String, Any> = emptyMap(),
- content: @Composable (T) -> Unit,
- ) {
- require(clazz !in clazzProviders) {
- "A `record` with the same `clazz` has already been added: ${clazz.simpleName}."
- }
- clazzProviders[clazz] = RecordClassProvider(clazz, featureMap, content)
- }
-
- /**
- * Returns an instance of recordProvider created from the record providers set on this builder.
- */
- @Suppress("UNCHECKED_CAST")
- public fun build(): (Any) -> NavRecord<*> = { key ->
- val recordClassProvider = clazzProviders[key::class] as? RecordClassProvider<Any>
- val recordProvider = providers[key] as? RecordProvider<Any>
- recordClassProvider?.run { NavRecord(key, featureMap, content) }
- ?: recordProvider?.run { NavRecord(key, featureMap, content) }
- ?: fallback.invoke(key)
- }
-}
-
-/** Add an record provider to the [RecordProviderBuilder] */
-public fun <T : Any> RecordProviderBuilder.record(
- key: T,
- featureMap: Map<String, Any> = emptyMap(),
- content: @Composable (T) -> Unit,
-) {
- addRecordProvider(key, featureMap, content)
-}
-
-/** Add an record provider to the [RecordProviderBuilder] */
-public inline fun <reified T : Any> RecordProviderBuilder.record(
- featureMap: Map<String, Any> = emptyMap(),
- noinline content: @Composable (T) -> Unit,
-) {
- addRecordProvider(T::class, featureMap, content)
-}
-
-/** Holds a Record class, featureMap, and content for that class */
-public data class RecordClassProvider<T : Any>(
- val clazz: KClass<T>,
- val featureMap: Map<String, Any>,
- val content: @Composable (T) -> Unit,
-)
-
-/** Holds a Record class, featureMap, and content for that key */
-public data class RecordProvider<T : Any>(
- val key: T,
- val featureMap: Map<String, Any>,
- val content: @Composable (T) -> Unit,
-)
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavLocalProvider.kt
similarity index 85%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavLocalProvider.kt
index 9c6443f..69451da 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavLocalProvider.kt
@@ -24,19 +24,19 @@
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
/**
- * Wraps the content of a [NavRecord] with a [SaveableStateHolder.SaveableStateProvider] to ensure
+ * Wraps the content of a [NavEntry] with a [SaveableStateHolder.SaveableStateProvider] to ensure
* that calls to [rememberSaveable] within the content work properly and that state can be saved.
*
- * This [NavContentWrapper] is the only one that is **required** as saving state is considered a
+ * This [NavLocalProvider] is the only one that is **required** as saving state is considered a
* non-optional feature.
*/
-public class SaveableStateNavContentWrapper : NavContentWrapper {
+public class SaveableStateNavLocalProvider : NavLocalProvider {
private var savedStateHolder: SaveableStateHolder? = null
private val refCount: MutableObjectIntMap<Any> = MutableObjectIntMap()
private var backstackSize = 0
@Composable
- override fun WrapBackStack(backStack: List<Any>) {
+ override fun ProvideToBackStack(backStack: List<Any>) {
DisposableEffect(key1 = backStack) {
refCount.clear()
onDispose {}
@@ -56,7 +56,7 @@
.getOrElse(key) {
error(
"Attempting to incorrectly dispose of backstack state in " +
- "SaveableStateNavContentWrapper"
+ "SaveableStateNavLocalProvider"
)
}
.minus(1)
@@ -67,8 +67,8 @@
}
@Composable
- public override fun <T : Any> WrapContent(record: NavRecord<T>) {
- val key = record.key
+ public override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
+ val key = entry.key
DisposableEffect(key1 = key) {
refCount[key] = refCount.getOrDefault(key, 0).plus(1)
onDispose {
@@ -85,7 +85,7 @@
.getOrElse(key) {
error(
"Attempting to incorrectly dispose of state associated with " +
- "key $key in SaveableStateNavContentWrapper"
+ "key $key in SaveableStateNavLocalProvider"
)
}
.minus(1)
@@ -94,6 +94,6 @@
}
val id: Int = rememberSaveable(key) { key.hashCode() + backstackSize }
- savedStateHolder?.SaveableStateProvider(id) { record.content.invoke(key) }
+ savedStateHolder?.SaveableStateProvider(id) { entry.content.invoke(key) }
}
}
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavLocalProvider.kt
similarity index 83%
rename from navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavContentWrapper.kt
rename to navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavLocalProvider.kt
index c651c3d..56febae 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SavedStateNavLocalProvider.kt
@@ -33,29 +33,29 @@
import androidx.savedstate.savedState
/**
- * Provides the content of a [NavRecord] with a [SavedStateRegistryOwner] and provides that
+ * Provides the content of a [NavEntry] with a [SavedStateRegistryOwner] and provides that
* [SavedStateRegistryOwner] as a [LocalSavedStateRegistryOwner] so that it is available within the
* content.
*/
-public object SavedStateNavContentWrapper : NavContentWrapper {
+public object SavedStateNavLocalProvider : NavLocalProvider {
@Composable
- override fun <T : Any> WrapContent(record: NavRecord<T>) {
- val key = record.key
+ override fun <T : Any> ProvideToEntry(entry: NavEntry<T>) {
+ val key = entry.key
val childRegistry by
rememberSaveable(
key,
stateSaver =
Saver(
save = { it.savedState },
- restore = { RecordSavedStateRegistry().apply { savedState = it } }
+ restore = { EntrySavedStateRegistry().apply { savedState = it } }
)
) {
- mutableStateOf(RecordSavedStateRegistry())
+ mutableStateOf(EntrySavedStateRegistry())
}
CompositionLocalProvider(LocalSavedStateRegistryOwner provides childRegistry) {
- record.content.invoke(key)
+ entry.content.invoke(key)
}
DisposableEffect(key1 = key) {
@@ -70,7 +70,7 @@
}
}
-private class RecordSavedStateRegistry : SavedStateRegistryOwner {
+private class EntrySavedStateRegistry : SavedStateRegistryOwner {
override val lifecycle: LifecycleRegistry = LifecycleRegistry(this)
val savedStateRegistryController = SavedStateRegistryController.create(this)
override val savedStateRegistry: SavedStateRegistry =
diff --git a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordProviderTest.kt b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/EntryProviderTest.kt
similarity index 64%
rename from navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordProviderTest.kt
rename to navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/EntryProviderTest.kt
index 67bb3e1..64e716e 100644
--- a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordProviderTest.kt
+++ b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/EntryProviderTest.kt
@@ -20,40 +20,40 @@
import kotlin.test.Test
import kotlin.test.fail
-class RecordProviderTest {
+class EntryProviderTest {
@Test
- fun recordProvider_withUniqueInitializers_returnsRecords() {
- val provider = recordProvider {
- record("first") {}
- record("second") {}
+ fun entryProvider_withUniqueInitializers_returnsEntries() {
+ val provider = entryProvider {
+ entry("first") {}
+ entry("second") {}
}
- val record1 = provider.invoke("first")
- val record2 = provider.invoke("second")
+ val entry1 = provider.invoke("first")
+ val entry2 = provider.invoke("second")
- assertThat(record1.key).isEqualTo("first")
- assertThat(record2.key).isEqualTo("second")
+ assertThat(entry1.key).isEqualTo("first")
+ assertThat(entry2.key).isEqualTo("second")
}
@Test
- fun recordProvider_withDuplicatedInitializers_throwsException() {
+ fun entryProvider_withDuplicatedInitializers_throwsException() {
try {
- recordProvider {
- record("first") {}
- record("first") {}
+ entryProvider {
+ entry("first") {}
+ entry("first") {}
}
fail("Expected `IllegalArgumentException` but no exception has been throw.")
} catch (e: IllegalArgumentException) {
assertThat(e)
.hasMessageThat()
- .isEqualTo("A `record` with the key `key` has already been added: first.")
+ .isEqualTo("An `entry` with the key `key` has already been added: first.")
}
}
@Test
- fun recordProvider_noInitializers_getsInvalidRecord() {
- val provider = recordProvider {}
+ fun entryProvider_noInitializers_getsInvalidEntry() {
+ val provider = entryProvider {}
try {
provider.invoke("something")
fail("Expected `IllegalStateException` but no exception has been throw.")
diff --git a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt
index b049d3f..863305e 100644
--- a/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt
+++ b/navigation3/navigation3/src/commonTest/kotlin/androidx/navigation3/RecordTest.kt
@@ -19,24 +19,24 @@
import androidx.kruth.assertThat
import kotlin.test.Test
-class RecordTest {
+class EntryTest {
@Test
fun getKey() {
- val record = NavRecord(key = "myKey", content = {})
- assertThat(record.key).isEqualTo("myKey")
+ val entry = NavEntry(key = "myKey", content = {})
+ assertThat(entry.key).isEqualTo("myKey")
}
@Test
fun getFeatureMap() {
- val record =
- NavRecord(
+ val entry =
+ NavEntry(
key = "myKey",
featureMap = mapOf("feature1" to 1, "feature2" to MyObject),
content = {}
)
- assertThat(record.featureMap["feature1"]).isEqualTo(1)
- assertThat(record.featureMap["feature2"]).isEqualTo(MyObject)
+ assertThat(entry.featureMap["feature1"]).isEqualTo(1)
+ assertThat(entry.featureMap["feature2"]).isEqualTo(MyObject)
}
object MyObject
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 3d44721..1dc4afe 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -853,9 +853,9 @@
private fun resetViewsAndModels(fileUri: Uri) {
if (pdfLoader != null) {
pdfLoaderCallbacks?.uri = fileUri
- paginatedView?.resetModels()
destroyContentModel()
}
+ paginatedView?.resetModels()
fastScrollView?.resetContents()
findInFileView?.resetFindInFile()
}
diff --git a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
index ec8233d..19b75a2 100644
--- a/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
+++ b/pdf/pdf-viewer-fragment/src/main/kotlin/androidx/pdf/viewer/fragment/PdfViewerFragmentV2.kt
@@ -52,7 +52,6 @@
import androidx.pdf.viewer.fragment.search.PdfSearchViewManager
import androidx.pdf.viewer.fragment.util.getCenter
import androidx.pdf.viewer.fragment.view.PdfViewManager
-import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -202,19 +201,9 @@
pdfViewManager =
PdfViewManager(
pdfView = pdfView,
- // TODO(b/385684706): Update colors for highlights
selectedHighlightColor =
- MaterialColors.getColor(
- pdfView,
- com.google.android.material.R.attr.colorPrimaryFixed,
- requireContext().getColor(R.color.selected_highlight_color)
- ),
- highlightColor =
- MaterialColors.getColor(
- pdfView,
- com.google.android.material.R.attr.colorSecondaryFixedDim,
- requireContext().getColor(R.color.highlight_color)
- )
+ requireContext().getColor(R.color.selected_highlight_color),
+ highlightColor = requireContext().getColor(R.color.highlight_color)
)
pdfSearchViewManager = PdfSearchViewManager(pdfSearchView)
diff --git a/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle
index 5aed355..88365c64 100644
--- a/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle
+++ b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/build.gradle
@@ -28,6 +28,10 @@
minSdk = 21
buildToolsVersion = AndroidXConfig.getDefaultAndroidConfig(project).buildToolsVersion
+ //TODO(b/389890488): This is added to suppress missing stub classes warning from ui-compose.
+ //Can be removed once linked bug is fixed.
+ optimization.keepRules.files += project.file('proguard-rules.pro')
+
bundle {
packageName = "androidx.privacysandbox.ui.integration.mediateesdkproviderwrapper"
diff --git a/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/proguard-rules.pro b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/proguard-rules.pro
new file mode 100644
index 0000000..13de6f6
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/mediateesdkproviderwrapper/proguard-rules.pro
@@ -0,0 +1,13 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+-dontwarn android.view.RenderNode
+-dontwarn android.view.DisplayListCanvas
+-dontwarn android.view.HardwareCanvas
diff --git a/privacysandbox/ui/integration-tests/testapp/build.gradle b/privacysandbox/ui/integration-tests/testapp/build.gradle
index 7ec0127..a9a297b 100644
--- a/privacysandbox/ui/integration-tests/testapp/build.gradle
+++ b/privacysandbox/ui/integration-tests/testapp/build.gradle
@@ -16,9 +16,9 @@
plugins {
id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
id("com.android.application")
id("org.jetbrains.kotlin.android")
- id("AndroidXComposePlugin")
}
android {
@@ -45,6 +45,7 @@
dependencies {
implementation(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.8.1")
+
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.6.0")
@@ -52,9 +53,7 @@
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.1")
implementation("androidx.recyclerview:recyclerview:1.3.2")
- implementation("androidx.compose.foundation:foundation-layout:1.7.5")
- implementation("androidx.compose.material3:material3-android:1.3.1")
- implementation("androidx.compose.ui:ui-util:1.7.5")
+ implementation("androidx.compose.material3:material3:1.3.1")
implementation(project(":privacysandbox:activity:activity-core"))
implementation(project(":privacysandbox:activity:activity-client"))
implementation(project(":privacysandbox:sdkruntime:sdkruntime-client"))
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 56e84a8..3559d29 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -23,6 +23,9 @@
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.fragment.app.Fragment
import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
@@ -46,6 +49,7 @@
private lateinit var sdkSandboxManager: SdkSandboxManagerCompat
private lateinit var activity: Activity
+ protected var providerUiOnTop by mutableStateOf(true)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -117,9 +121,8 @@
}
open fun handleDrawerStateChange(isDrawerOpen: Boolean) {
- getSandboxedSdkViews().forEach {
- it.orderProviderUiAboveClientUi(!isDrawerOpen && isZOrderOnTop)
- }
+ providerUiOnTop = !isDrawerOpen && !isZOrderBelowToggleChecked
+ getSandboxedSdkViews().forEach { it.orderProviderUiAboveClientUi(providerUiOnTop) }
}
private inner class TestEventListener(val view: SandboxedSdkView) :
@@ -147,7 +150,7 @@
private const val MEDIATEE_SDK_NAME =
"androidx.privacysandbox.ui.integration.mediateesdkproviderwrapper"
const val TAG = "TestSandboxClient"
- var isZOrderOnTop = true
+ var isZOrderBelowToggleChecked = false
@AdType var currentAdType = AdType.BASIC_NON_WEBVIEW
@MediationOption var currentMediationOption = MediationOption.NON_MEDIATED
var shouldDrawViewabilityLayer = false
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index 8ba632c..8467ece 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -237,7 +237,7 @@
private fun initializeZOrderToggleButton() {
zOrderToggleButton.setOnCheckedChangeListener { _, isChecked ->
- BaseFragment.isZOrderOnTop = !isChecked
+ BaseFragment.isZOrderBelowToggleChecked = isChecked
}
}
@@ -290,8 +290,24 @@
private fun selectCuj(menuItem: MenuItem) {
when (menuItem.itemId) {
- R.id.item_resize -> switchContentFragment(ResizeFragment(), menuItem.title)
- R.id.item_scroll -> switchContentFragment(ScrollFragment(), menuItem.title)
+ R.id.item_resize ->
+ if (useCompose) {
+ switchContentFragment(
+ ResizeComposeFragment(),
+ "${menuItem.title} ${getString(R.string.compose)}"
+ )
+ } else {
+ switchContentFragment(ResizeFragment(), menuItem.title)
+ }
+ R.id.item_scroll ->
+ if (useCompose) {
+ switchContentFragment(
+ ScrollComposeFragment(),
+ "${menuItem.title} ${getString(R.string.compose)}"
+ )
+ } else {
+ switchContentFragment(ScrollFragment(), menuItem.title)
+ }
R.id.item_pooling_container ->
switchContentFragment(PoolingContainerFragment(), menuItem.title)
R.id.item_fullscreen ->
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index abdf0d4..fd5d6e9 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -38,7 +38,7 @@
override fun handleDrawerStateChange(isDrawerOpen: Boolean) {
super.handleDrawerStateChange(isDrawerOpen)
- (recyclerView.adapter as CustomAdapter).zOrderOnTop = !isDrawerOpen && isZOrderOnTop
+ (recyclerView.adapter as CustomAdapter).zOrderOnTop = providerUiOnTop
}
override fun handleLoadAdFromDrawer(
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeComposeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeComposeFragment.kt
new file mode 100644
index 0000000..c537976
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeComposeFragment.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2024 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.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+class ResizeComposeFragment : BaseFragment() {
+
+ private var adapter: SandboxedUiAdapter? by mutableStateOf(null)
+ private var adEventText by mutableStateOf("")
+ private var bannerDimension by mutableStateOf(BannerDimension())
+ private val onBannerDimensionChanged: (BannerDimension) -> Unit = { currentDimension ->
+ val displayMetrics = resources.displayMetrics
+ val maxSizePixels = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels)
+ val newSize = { currentSize: Int, maxSize: Int ->
+ (currentSize + (100..200).random()) % maxSize
+ }
+ val newWidth = newSize(currentDimension.width.value.toInt(), maxSizePixels).dp
+ val newHeight = newSize(currentDimension.height.value.toInt(), maxSizePixels).dp
+ bannerDimension = BannerDimension(newWidth, newHeight)
+ }
+
+ override fun handleLoadAdFromDrawer(
+ adType: Int,
+ mediationOption: Int,
+ drawViewabilityLayer: Boolean
+ ) {
+ currentAdType = adType
+ currentMediationOption = mediationOption
+ shouldDrawViewabilityLayer = drawViewabilityLayer
+ setAdAdapter()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ setAdAdapter()
+ return ComposeView(requireContext()).apply {
+ // Dispose of the Composition when the view's LifecycleOwner is destroyed
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent { ResizeableBannerAd(adapter, bannerDimension, onBannerDimensionChanged) }
+ }
+ }
+
+ @Composable
+ fun ResizeableBannerAd(
+ adapter: SandboxedUiAdapter?,
+ bannerDimension: BannerDimension,
+ onResizeClicked: (BannerDimension) -> Unit
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start
+ ) {
+ val sandboxedSdkUiModifier =
+ if (bannerDimension.height != 0.dp && bannerDimension.width != 0.dp) {
+ Modifier.width(bannerDimension.width)
+ .weight(
+ 1f,
+ )
+ } else {
+ Modifier.fillMaxWidth().weight(1f)
+ }
+
+ Text("Ad state: $adEventText")
+ if (adapter != null) {
+ SandboxedSdkUi(
+ adapter,
+ sandboxedSdkUiModifier,
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener =
+ object : SandboxedSdkViewEventListener {
+ override fun onUiDisplayed() {
+ adEventText = "Ad is visible"
+ }
+
+ override fun onUiError(error: Throwable) {
+ adEventText = "Error loading ad : ${error.message}"
+ }
+
+ override fun onUiClosed() {
+ adEventText = "Ad session is closed"
+ }
+ },
+ )
+ }
+ Button(onClick = { onResizeClicked(bannerDimension) }) { Text("Resize") }
+ }
+ }
+
+ private fun setAdAdapter() {
+ val coroutineScope = MainScope()
+ coroutineScope.launch {
+ adapter =
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ getSdkApi()
+ .loadBannerAd(
+ currentAdType,
+ currentMediationOption,
+ false,
+ shouldDrawViewabilityLayer,
+ )
+ )
+ }
+ }
+
+ data class BannerDimension(val width: Dp = 0.dp, val height: Dp = 0.dp)
+}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt
new file mode 100644
index 0000000..6acfb9c
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2025 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.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+class ScrollComposeFragment : BaseFragment() {
+
+ private var bottomBannerAdapter: SandboxedUiAdapter? by mutableStateOf(null)
+ private var scrollBannerAdapter: SandboxedUiAdapter? by mutableStateOf(null)
+
+ override fun handleLoadAdFromDrawer(
+ adType: Int,
+ mediationOption: Int,
+ drawViewabilityLayer: Boolean
+ ) {
+ currentAdType = adType
+ currentMediationOption = mediationOption
+ shouldDrawViewabilityLayer = drawViewabilityLayer
+ setAdapter()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ setAdapter()
+ return ComposeView(requireContext()).apply {
+ // Dispose of the Composition when the view's LifecycleOwner is destroyed
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start
+ ) {
+ Column(modifier = Modifier.weight(0.8f).verticalScroll(rememberScrollState())) {
+ scrollBannerAdapter?.let {
+ SandboxedSdkUi(
+ it,
+ Modifier.fillMaxWidth().height(200.dp),
+ providerUiOnTop = providerUiOnTop
+ )
+ }
+ Text(stringResource(R.string.long_text), Modifier.padding(vertical = 16.dp))
+ }
+ bottomBannerAdapter?.let {
+ SandboxedSdkUi(it, Modifier.weight(0.2f), providerUiOnTop = providerUiOnTop)
+ }
+ }
+ }
+ }
+ }
+
+ private fun setAdapter() {
+ val coroutineScope = MainScope()
+ coroutineScope.launch {
+ bottomBannerAdapter =
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ getSdkApi()
+ .loadBannerAd(
+ currentAdType,
+ currentMediationOption,
+ false,
+ shouldDrawViewabilityLayer,
+ )
+ )
+ scrollBannerAdapter =
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ getSdkApi()
+ .loadBannerAd(
+ currentAdType,
+ currentMediationOption,
+ false,
+ shouldDrawViewabilityLayer,
+ )
+ )
+ }
+ }
+}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
index cfdbb065..143c337 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
@@ -16,6 +16,7 @@
<resources>
<string name="resize_cuj">Resize CUJ</string>
+ <string name="compose">Compose</string>
<string name="scroll_cuj">Scroll CUJ</string>
<string name="poolingcontainer_cuj">PoolingContainer CUJ</string>
<string name="fullscreen_cuj">Fullscreen CUJ</string>
diff --git a/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle
index 96d56fa..0532e9c 100644
--- a/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle
+++ b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/build.gradle
@@ -28,6 +28,10 @@
minSdk = 21
buildToolsVersion = AndroidXConfig.getDefaultAndroidConfig(project).buildToolsVersion
+ //TODO(b/389890488): This is added to suppress missing stub classes warning from ui-compose.
+ //Can be removed once linked bug is fixed.
+ optimization.keepRules.files += project.file('proguard-rules.pro')
+
bundle {
packageName = "androidx.privacysandbox.ui.integration.testsdkproviderwrapper"
diff --git a/privacysandbox/ui/integration-tests/testsdkproviderwrapper/proguard-rules.pro b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/proguard-rules.pro
new file mode 100644
index 0000000..13de6f6
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testsdkproviderwrapper/proguard-rules.pro
@@ -0,0 +1,13 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+-dontwarn android.view.RenderNode
+-dontwarn android.view.DisplayListCanvas
+-dontwarn android.view.HardwareCanvas
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
index a3d0e4d..94a15f0 100644
--- a/privacysandbox/ui/ui-client/api/current.txt
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -10,6 +10,10 @@
package androidx.privacysandbox.ui.client.view {
+ public final class SandboxedSdkUiKt {
+ method @androidx.compose.runtime.Composable public static void SandboxedSdkUi(androidx.privacysandbox.ui.core.SandboxedUiAdapter sandboxedUiAdapter, optional androidx.compose.ui.Modifier modifier, optional boolean providerUiOnTop, optional androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener? sandboxedSdkViewEventListener);
+ }
+
public final class SandboxedSdkView extends android.view.ViewGroup {
ctor public SandboxedSdkView(android.content.Context context);
ctor public SandboxedSdkView(android.content.Context context, optional android.util.AttributeSet? attrs);
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
index a3d0e4d..94a15f0 100644
--- a/privacysandbox/ui/ui-client/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -10,6 +10,10 @@
package androidx.privacysandbox.ui.client.view {
+ public final class SandboxedSdkUiKt {
+ method @androidx.compose.runtime.Composable public static void SandboxedSdkUi(androidx.privacysandbox.ui.core.SandboxedUiAdapter sandboxedUiAdapter, optional androidx.compose.ui.Modifier modifier, optional boolean providerUiOnTop, optional androidx.privacysandbox.ui.client.view.SandboxedSdkViewEventListener? sandboxedSdkViewEventListener);
+ }
+
public final class SandboxedSdkView extends android.view.ViewGroup {
ctor public SandboxedSdkView(android.content.Context context);
ctor public SandboxedSdkView(android.content.Context context, optional android.util.AttributeSet? attrs);
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index bec5091..7546329 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -27,12 +27,14 @@
id("AndroidXPlugin")
id("com.android.library")
id("org.jetbrains.kotlin.android")
+ id("AndroidXComposePlugin")
}
dependencies {
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.8.1")
api("androidx.core:core:1.12.0")
+ api("androidx.compose.ui:ui:1.7.6")
api("androidx.lifecycle:lifecycle-common:2.6.2")
api("androidx.customview:customview-poolingcontainer:1.0.0")
api(project(":privacysandbox:ui:ui-core"))
@@ -52,6 +54,9 @@
androidTestImplementation(project(":appcompat:appcompat"))
androidTestImplementation(project(":privacysandbox:ui:ui-provider"))
androidTestImplementation(project(":privacysandbox:ui:integration-tests:testingutils"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.6")
+ androidTestImplementation("androidx.compose.foundation:foundation-layout:1.7.6")
+ debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.6")
}
android {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
index 1f5dbd3..c245d56 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
+++ b/privacysandbox/ui/ui-client/src/androidTest/AndroidManifest.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,18 +16,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- This override is okay because the associated tests only run on T+ -->
- <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator, androidx.test.uiautomator" />
- <application android:supportsRtl="true"
+ <application
+ android:supportsRtl="true"
android:theme="@style/Theme.AppCompat">
- <activity
- android:name="androidx.privacysandbox.ui.client.test.UiLibActivity"
- android:configChanges="orientation|screenSize"
- android:exported="true"/>
- <activity android:name=".SecondActivity"
- android:exported="true"/>
+ <activity
+ android:name="androidx.privacysandbox.ui.client.test.UiLibActivity"
+ android:configChanges="orientation|screenSize"
+ android:exported="true" />
+ <activity
+ android:name="androidx.privacysandbox.ui.client.test.SecondActivity"
+ android:exported="true" />
+ <activity
+ android:name="androidx.privacysandbox.ui.client.test.UiLibComposeActivity"
+ android:configChanges="orientation|screenSize"
+ android:exported="true" />
</application>
-
<queries>
<package android:name="androidx.privacysandbox.ui.client.test" />
</queries>
+
+ <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator, androidx.test.uiautomator" />
</manifest>
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/FailingTestSandboxedUiAdapter.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/FailingTestSandboxedUiAdapter.kt
new file mode 100644
index 0000000..79dd0c8
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/FailingTestSandboxedUiAdapter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.privacysandbox.ui.client.test
+
+import android.content.Context
+import android.os.IBinder
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
+import java.util.concurrent.Executor
+
+class FailingTestSandboxedUiAdapter : AbstractSandboxedUiAdapter() {
+ override fun openSession(
+ context: Context,
+ windowInputToken: IBinder,
+ initialWidth: Int,
+ initialHeight: Int,
+ isZOrderOnTop: Boolean,
+ clientExecutor: Executor,
+ client: SandboxedUiAdapter.SessionClient
+ ) {
+ clientExecutor.execute { client.onSessionError(Exception("Error in openSession()")) }
+ }
+}
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkUiTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkUiTest.kt
new file mode 100644
index 0000000..e057517
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkUiTest.kt
@@ -0,0 +1,558 @@
+/*
+ * Copyright 2024 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.privacysandbox.ui.client.test
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.util.DisplayMetrics
+import android.util.TypedValue
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.privacysandbox.ui.client.test.SandboxedSdkViewTest.Companion.SHORTEST_TIME_BETWEEN_SIGNALS_MS
+import androidx.privacysandbox.ui.client.test.SandboxedSdkViewTest.Companion.TIMEOUT
+import androidx.privacysandbox.ui.client.test.SandboxedSdkViewTest.Companion.UI_INTENSIVE_TIMEOUT
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.client.view.SandboxedSdkView
+import androidx.privacysandbox.ui.integration.testingutils.TestEventListener
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import org.hamcrest.Matchers.instanceOf
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO(b/374919355): Create a common test framework for testing Compose and View UI lib constructs
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SandboxedSdkUiTest {
+ @get:Rule val composeTestRule = createAndroidComposeRule<UiLibComposeActivity>()
+ private var testSandboxedUiAdapter by mutableStateOf(TestSandboxedUiAdapter())
+ private var eventListener by mutableStateOf(TestEventListener())
+ private var providerUiOnTop by mutableStateOf(true)
+ private var size by mutableStateOf(20.dp)
+ private lateinit var uiDevice: UiDevice
+
+ @Before
+ fun setup() {
+ testSandboxedUiAdapter = TestSandboxedUiAdapter()
+ eventListener = TestEventListener()
+ providerUiOnTop = true
+ uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ }
+
+ @Test
+ fun eventListenerErrorTest() {
+ composeTestRule.setContent {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = FailingTestSandboxedUiAdapter(),
+ modifier = Modifier,
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ assertThat(eventListener.errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(eventListener.error?.message).isEqualTo("Error in openSession()")
+ }
+
+ @Test
+ fun addEventListenerTest() {
+ // Initially no events are received when the session is not open.
+ assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+ // When session is open, the events are received
+ addNodeToLayout()
+ assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ @Test
+ fun sessionNotOpenedWhenWindowIsNotVisibleTest() {
+ // the window is not visible when the activity is in the CREATED state.
+ composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionNotOpened()
+ // the window becomes visible when the activity is in the STARTED state.
+ composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.STARTED)
+ testSandboxedUiAdapter.assertSessionOpened()
+ }
+
+ @Test
+ fun onAttachedToWindowTest() {
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java))
+ .check(matches(isDisplayed()))
+ .check(matches(hasChildCount(1)))
+ .check { view, exception ->
+ if (
+ view.layoutParams.width != WRAP_CONTENT ||
+ view.layoutParams.height != WRAP_CONTENT
+ ) {
+ throw exception
+ }
+ }
+ }
+
+ @Test
+ fun childViewRemovedOnErrorTest() {
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check(matches(hasChildCount(1)))
+ composeTestRule.activityRule.withActivity {
+ testSandboxedUiAdapter.internalClient!!.onSessionError(Exception())
+ }
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check(matches(hasChildCount(0)))
+ }
+
+ @Test
+ fun onZOrderChangedTest() {
+ addNodeToLayout()
+ // When session is opened, the provider should not receive a Z-order notification.
+ testSandboxedUiAdapter.assertSessionOpened()
+ val session = testSandboxedUiAdapter.testSession
+ assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+ assertThat(testSandboxedUiAdapter.isZOrderOnTop).isTrue()
+ // When state changes to false, the provider should be notified.
+ providerUiOnTop = false
+ composeTestRule.waitForIdle()
+ assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
+ // When state changes back to true, the provider should be notified.
+ session?.zOrderChangedLatch = CountDownLatch(1)
+ providerUiOnTop = true
+ composeTestRule.waitForIdle()
+ assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(testSandboxedUiAdapter.isZOrderOnTop).isTrue()
+ }
+
+ @Test
+ fun setZOrderNotOnTopBeforeOpeningSessionTest() {
+ providerUiOnTop = false
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ val session = testSandboxedUiAdapter.testSession
+ // The initial Z-order state is passed to the session, but notifyZOrderChanged is not called
+ assertThat(session?.zOrderChangedLatch?.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+ assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
+ }
+
+ @Test
+ fun setZOrderNotOnTopWhileSessionLoadingTest() {
+ testSandboxedUiAdapter.delayOpenSessionCallback = true
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ providerUiOnTop = false
+ composeTestRule.waitForIdle()
+ val session = testSandboxedUiAdapter.testSession!!
+ assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+ composeTestRule.activityRule.withActivity { testSandboxedUiAdapter.sendOnSessionOpened() }
+ // After session has opened, the pending Z order changed made while loading is notified
+ // to the session.
+ assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
+ }
+
+ @Test
+ fun onConfigurationChangedSessionRemainsOpened() {
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ // newWindow() will be triggered by a window state change, even if the activity handles
+ // orientation changes without recreating the activity.
+ uiDevice.performActionAndWait(
+ { uiDevice.setOrientationLeft() },
+ Until.newWindow(),
+ UI_INTENSIVE_TIMEOUT
+ )
+ testSandboxedUiAdapter.assertSessionNotClosed()
+ uiDevice.performActionAndWait(
+ { uiDevice.setOrientationNatural() },
+ Until.newWindow(),
+ UI_INTENSIVE_TIMEOUT
+ )
+ testSandboxedUiAdapter.assertSessionNotClosed()
+ }
+
+ @Test
+ fun onConfigurationChangedTestSameConfigurationTest() {
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ composeTestRule.activityRule.withActivity {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ }
+ assertThat(testSandboxedUiAdapter.wasOnConfigChangedCalled()).isFalse()
+ }
+
+ @Test
+ fun onPaddingSetTest() {
+ var padding by mutableStateOf(0.dp)
+ composeTestRule.setContent {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.padding(all = padding),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ testSandboxedUiAdapter.assertSessionOpened()
+ padding = 10.dp
+ composeTestRule.waitForIdle()
+ assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isTrue()
+ }
+
+ @Test
+ fun signalsSentWhenPaddingApplied() {
+ var padding by mutableStateOf(0.dp)
+ composeTestRule.setContent {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.padding(all = padding),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ testSandboxedUiAdapter.assertSessionOpened()
+ val session = testSandboxedUiAdapter.testSession
+ session?.runAndRetrieveNextUiChange {
+ padding = 10.dp
+ composeTestRule.waitForIdle()
+ }
+ assertThat(session?.shortestGapBetweenUiChangeEvents)
+ .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+ }
+
+ @Test
+ fun onLayoutTestWithSizeChangeTest() {
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java))
+ .check(matches(isDisplayed()))
+ .check(matches(hasChildCount(1)))
+ .check { view, exception ->
+ val expectedSize = size.toPx(view.context.resources.displayMetrics)
+ if (view.width != expectedSize || view.height != expectedSize) {
+ throw exception
+ }
+ }
+ size = 30.dp
+ composeTestRule.waitForIdle()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java))
+ .check(matches(isDisplayed()))
+ .check(matches(hasChildCount(1)))
+ .check { view, exception ->
+ val expectedSize = size.toPx(view.context.resources.displayMetrics)
+ if (view.width != expectedSize || view.height != expectedSize) {
+ throw exception
+ }
+ }
+ assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isTrue()
+ }
+
+ @Test
+ fun onLayoutTestNoSizeChangeTest() {
+ size = 20.dp
+ addNodeToLayout()
+ testSandboxedUiAdapter.assertSessionOpened()
+ size = 20.dp
+ composeTestRule.waitForIdle()
+ assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isFalse()
+ }
+
+ @Test
+ fun onLayoutTestViewShiftWithoutSizeChangeTest() {
+ var offset by mutableStateOf(0.dp)
+ composeTestRule.setContent {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.offset(offset),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ testSandboxedUiAdapter.assertSessionOpened()
+ offset = 10.dp
+ composeTestRule.waitForIdle()
+ assertThat(testSandboxedUiAdapter.wasNotifyResizedCalled()).isFalse()
+ }
+
+ @Test
+ fun onSdkRequestsResizeTest() {
+ val boxSize = 300.dp
+ composeTestRule.setContent {
+ Box(modifier = Modifier.requiredSize(boxSize)) {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier,
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ }
+ testSandboxedUiAdapter.assertSessionOpened()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(view.width).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+ assertThat(view.height).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+ }
+ composeTestRule.activityRule.withActivity {
+ testSandboxedUiAdapter.testSession?.requestResize(200, 200) as Any
+ }
+ composeTestRule.waitForIdle()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(view.width).isEqualTo(200)
+ assertThat(view.height).isEqualTo(200)
+ }
+ }
+
+ @Test
+ fun requestResizeWithMeasureSpecAtMostExceedsParentBoundsTest() {
+ val boxSize = 300.dp
+ composeTestRule.setContent {
+ Box(modifier = Modifier.requiredSize(boxSize)) {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier,
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ }
+ testSandboxedUiAdapter.assertSessionOpened()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(view.width).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+ assertThat(view.height).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+ }
+ composeTestRule.activityRule.withActivity {
+ val newSize = (boxSize + 10.dp).toPx(resources.displayMetrics)
+ testSandboxedUiAdapter.testSession?.requestResize(newSize, newSize) as Any
+ }
+ composeTestRule.waitForIdle()
+ // the resize is constrained by the parent's size
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(view.width).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+ assertThat(view.height).isEqualTo(boxSize.toPx(view.context.resources.displayMetrics))
+ }
+ }
+
+ @Test
+ fun requestResizeWithMeasureSpecExactlyTest() {
+ var boxWidth = 0.dp
+ var boxHeight = 0.dp
+ composeTestRule.setContent {
+ // Get local density from composable
+ val localDensity = LocalDensity.current
+ Box(
+ modifier =
+ Modifier.fillMaxSize().onGloballyPositioned { coordinates ->
+ boxWidth = with(localDensity) { coordinates.size.width.toDp() }
+ boxHeight = with(localDensity) { coordinates.size.height.toDp() }
+ }
+ ) {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.fillMaxSize(),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ }
+ testSandboxedUiAdapter.assertSessionOpened()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(view.width).isEqualTo(boxWidth.toPx(view.context.resources.displayMetrics))
+ assertThat(view.height).isEqualTo(boxHeight.toPx(view.context.resources.displayMetrics))
+ }
+ composeTestRule.activityRule.withActivity {
+ val newBoxWidth = (boxWidth - 10.dp).toPx(resources.displayMetrics)
+ val newBoxHeight = (boxHeight - 10.dp).toPx(resources.displayMetrics)
+ testSandboxedUiAdapter.testSession?.requestResize(newBoxWidth, newBoxHeight) as Any
+ }
+ composeTestRule.waitForIdle()
+ // the request is a no-op when the MeasureSpec is EXACTLY
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(view.width).isEqualTo((boxWidth).toPx(view.context.resources.displayMetrics))
+ assertThat(view.height)
+ .isEqualTo((boxHeight).toPx(view.context.resources.displayMetrics))
+ }
+ }
+
+ @Test
+ fun sandboxedSdkViewIsTransitionGroupTest() {
+ addNodeToLayout()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, exception ->
+ if (!(view as ViewGroup).isTransitionGroup) {
+ throw exception
+ }
+ }
+ }
+
+ @Test
+ fun signalsNotSentWhenViewUnchangedTest() {
+ addNodeToLayout()
+ val session = testSandboxedUiAdapter.testSession
+ session?.runAndRetrieveNextUiChange {}
+ session?.assertNoSubsequentUiChanges()
+ }
+
+ /**
+ * Shifts the view partially off screen and verifies that the reported onScreenGeometry is
+ * cropped accordingly.
+ */
+ @Test
+ fun correctSignalsSentForOnScreenGeometryWhenViewOffScreenTest() {
+ var xOffset by mutableStateOf(0.dp)
+ var yOffset by mutableStateOf(0.dp)
+ val clippedWidth = 300.dp
+ val clippedHeight = 400.dp
+ composeTestRule.setContent {
+ Box(modifier = Modifier.size(width = clippedWidth, height = clippedHeight)) {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.offset(x = xOffset, y = yOffset),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ }
+ var initialHeight = 0
+ var initialWidth = 0
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ initialHeight = view.height
+ initialWidth = view.width
+ }
+ val session = testSandboxedUiAdapter.testSession
+ val sandboxedSdkViewUiInfo =
+ session?.runAndRetrieveNextUiChange {
+ xOffset = 100.dp
+ yOffset = 200.dp
+ composeTestRule.waitForIdle()
+ }
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, _ ->
+ assertThat(sandboxedSdkViewUiInfo?.uiContainerWidth)
+ .isEqualTo(clippedWidth.toPx(view.context.resources.displayMetrics))
+ assertThat(sandboxedSdkViewUiInfo?.uiContainerHeight)
+ .isEqualTo(clippedHeight.toPx(view.context.resources.displayMetrics))
+ assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.height()?.toFloat())
+ .isEqualTo(initialHeight - yOffset.toPx(view.context.resources.displayMetrics))
+ assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.width()?.toFloat())
+ .isEqualTo(initialWidth - xOffset.toPx(view.context.resources.displayMetrics))
+ }
+ }
+
+ @Test
+ fun signalsSentWhenPositionChangesTest() {
+ var offset by mutableStateOf(0.dp)
+ composeTestRule.setContent {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.offset(x = offset),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ val session = testSandboxedUiAdapter.testSession
+ val sandboxedSdkViewUiInfo =
+ session?.runAndRetrieveNextUiChange {
+ offset = 10.dp
+ composeTestRule.waitForIdle()
+ }
+ val containerWidth = sandboxedSdkViewUiInfo?.uiContainerWidth ?: 0
+ val onScreenWidth = sandboxedSdkViewUiInfo?.onScreenGeometry?.width()?.toFloat()
+ Espresso.onView(instanceOf(SandboxedSdkView::class.java)).check { view, exception ->
+ val expectedWidth =
+ (containerWidth - offset.toPx(view.context.resources.displayMetrics)).toFloat()
+ if (expectedWidth != onScreenWidth) {
+ throw exception
+ }
+ }
+ }
+
+ /**
+ * Creates many UI changes and ensures that these changes are not sent more frequently than
+ * expected.
+ */
+ @Test
+ @SuppressLint("BanThreadSleep") // Deliberate delay for testing
+ fun signalsNotSentMoreFrequentlyThanLimitTest() {
+ addNodeToLayout()
+ val session = testSandboxedUiAdapter.testSession!!
+ for (i in 1..10) {
+ size += (i * 10).dp
+ composeTestRule.waitForIdle()
+ Thread.sleep(100)
+ }
+ assertThat(session.shortestGapBetweenUiChangeEvents)
+ .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+ }
+
+ @Test
+ fun signalsSentWhenHostActivityStateChangesTest() {
+ addNodeToLayout()
+ val session = testSandboxedUiAdapter.testSession
+ session?.runAndRetrieveNextUiChange {}
+ // Replace the first activity with a new activity. The onScreenGeometry should now be empty.
+ var sandboxedSdkViewUiInfo =
+ session?.runAndRetrieveNextUiChange {
+ composeTestRule.activityRule.scenario.onActivity {
+ val intent = Intent(it, SecondActivity::class.java)
+ it.startActivity(intent)
+ }
+ }
+ assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.isEmpty).isTrue()
+ // Return to the first activity. The onScreenGeometry should now be non-empty.
+ sandboxedSdkViewUiInfo = session?.runAndRetrieveNextUiChange { uiDevice.pressBack() }
+ assertThat(sandboxedSdkViewUiInfo?.onScreenGeometry?.isEmpty).isFalse()
+ }
+
+ private fun addNodeToLayout() {
+ composeTestRule.setContent {
+ SandboxedSdkUi(
+ sandboxedUiAdapter = testSandboxedUiAdapter,
+ modifier = Modifier.requiredSize(size),
+ providerUiOnTop = providerUiOnTop,
+ sandboxedSdkViewEventListener = eventListener
+ )
+ }
+ }
+
+ private fun Dp.toPx(displayMetrics: DisplayMetrics) =
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics).roundToInt()
+}
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index 84015cb..9002a19 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.privacysandbox.ui.client.test
import android.annotation.SuppressLint
@@ -55,19 +54,10 @@
import org.junit.Test
import org.junit.runner.RunWith
+// TODO(b/374919355): Create a common test for View and Compose
@RunWith(AndroidJUnit4::class)
@LargeTest
class SandboxedSdkViewTest {
-
- companion object {
- const val TIMEOUT = 1000.toLong()
-
- // Longer timeout used for expensive operations like device rotation.
- const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
-
- const val SHORTEST_TIME_BETWEEN_SIGNALS_MS = 200
- }
-
private lateinit var uiDevice: UiDevice
private lateinit var context: Context
private lateinit var view: SandboxedSdkView
@@ -77,23 +67,8 @@
private lateinit var linearLayout: LinearLayout
private var mainLayoutWidth = -1
private var mainLayoutHeight = -1
-
@get:Rule var activityScenarioRule = ActivityScenarioRule(UiLibActivity::class.java)
- class FailingTestSandboxedUiAdapter : AbstractSandboxedUiAdapter() {
- override fun openSession(
- context: Context,
- windowInputToken: IBinder,
- initialWidth: Int,
- initialHeight: Int,
- isZOrderOnTop: Boolean,
- clientExecutor: Executor,
- client: SandboxedUiAdapter.SessionClient
- ) {
- clientExecutor.execute { client.onSessionError(Exception("Error in openSession()")) }
- }
- }
-
@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -125,20 +100,15 @@
fun addAndRemoveEventListenerTest() {
// Initially no events are received when the session is not open.
assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
-
// When session is open, the events are received
addViewToLayout()
assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
// Remove the view from layout to close the session.
removeAllViewsFromLayout()
assertThat(eventListener.sessionClosedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
eventListener.uiDisplayedLatch = CountDownLatch(1)
-
// Remove the listener from the view.
view.setEventListener(null)
-
// Add view to layout again to start the session. The latches will not count down this time.
addViewToLayout()
assertThat(eventListener.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
@@ -150,16 +120,13 @@
val eventListener2 = TestEventListener()
view.setEventListener(eventListener1)
view.setEventListener(eventListener2)
-
activityScenarioRule.withActivity { view.setAdapter(FailingTestSandboxedUiAdapter()) }
addViewToLayout()
assertThat(eventListener1.errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
assertThat(eventListener2.errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
activityScenarioRule.withActivity { view.setAdapter(testSandboxedUiAdapter) }
assertThat(eventListener1.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
assertThat(eventListener2.uiDisplayedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
removeAllViewsFromLayout()
assertThat(eventListener1.sessionClosedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS))
.isFalse()
@@ -189,11 +156,9 @@
fun childViewRemovedOnErrorTest() {
assertTrue(view.childCount == 0)
addViewToLayout()
-
testSandboxedUiAdapter.assertSessionOpened()
assertTrue(view.childCount == 1)
assertTrue(view.layoutParams == layoutParams)
-
activityScenarioRule.withActivity {
testSandboxedUiAdapter.internalClient!!.onSessionError(Exception())
assertTrue(view.childCount == 0)
@@ -203,19 +168,16 @@
@Test
fun onZOrderChangedTest() {
addViewToLayout()
-
// When session is opened, the provider should not receive a Z-order notification.
testSandboxedUiAdapter.assertSessionOpened()
val session = testSandboxedUiAdapter.testSession!!
val adapter = testSandboxedUiAdapter
assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
assertThat(adapter.isZOrderOnTop).isTrue()
-
// When state changes to false, the provider should be notified.
view.orderProviderUiAboveClientUi(false)
assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
assertThat(adapter.isZOrderOnTop).isFalse()
-
// When state changes back to true, the provider should be notified.
session.zOrderChangedLatch = CountDownLatch(1)
view.orderProviderUiAboveClientUi(true)
@@ -226,13 +188,11 @@
@Test
fun onZOrderUnchangedTest() {
addViewToLayout()
-
// When session is opened, the provider should not receive a Z-order notification.
testSandboxedUiAdapter.assertSessionOpened()
val session = testSandboxedUiAdapter.testSession!!
val adapter = testSandboxedUiAdapter
assertThat(adapter.isZOrderOnTop).isTrue()
-
// When Z-order state is unchanged, the provider should not be notified.
session.zOrderChangedLatch = CountDownLatch(1)
view.orderProviderUiAboveClientUi(true)
@@ -246,7 +206,6 @@
addViewToLayout()
testSandboxedUiAdapter.assertSessionOpened()
val session = testSandboxedUiAdapter.testSession!!
-
// The initial Z-order state is passed to the session, but notifyZOrderChanged is not called
assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
assertThat(testSandboxedUiAdapter.isZOrderOnTop).isFalse()
@@ -261,7 +220,6 @@
val session = testSandboxedUiAdapter.testSession!!
assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
activityScenarioRule.withActivity { testSandboxedUiAdapter.sendOnSessionOpened() }
-
// After session has opened, the pending Z order changed made while loading is notified
// th the session.
assertThat(session.zOrderChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
@@ -279,12 +237,13 @@
Until.newWindow(),
UI_INTENSIVE_TIMEOUT
)
- testSandboxedUiAdapter.assertSessionOpened()
+ testSandboxedUiAdapter.assertSessionNotClosed()
uiDevice.performActionAndWait(
{ uiDevice.setOrientationNatural() },
Until.newWindow(),
UI_INTENSIVE_TIMEOUT
)
+ testSandboxedUiAdapter.assertSessionNotClosed()
}
@Test
@@ -301,7 +260,6 @@
fun overrideProviderViewLayoutParams() {
val providerViewWidth = (0..1000).random()
val providerViewHeight = (0..1000).random()
-
class CustomSession : AbstractSandboxedUiAdapter.AbstractSession() {
override val view = View(context)
@@ -309,7 +267,6 @@
view.layoutParams = LinearLayout.LayoutParams(providerViewWidth, providerViewHeight)
}
}
-
class CustomUiAdapter : AbstractSandboxedUiAdapter() {
override fun openSession(
context: Context,
@@ -323,11 +280,9 @@
clientExecutor.execute { client.onSessionOpened(CustomSession()) }
}
}
-
view.setAdapter(CustomUiAdapter())
addViewToLayout(waitToBeActive = true)
val contentView = view.getChildAt(0)
-
assertThat(contentView.layoutParams.width).isNotEqualTo(providerViewWidth)
assertThat(contentView.layoutParams.height).isNotEqualTo(providerViewHeight)
assertThat(contentView.layoutParams.width).isEqualTo(LinearLayout.LayoutParams.WRAP_CONTENT)
@@ -341,7 +296,6 @@
val initialWidth = 100
val initialHeight = 100
view.layoutParams = LinearLayout.LayoutParams(initialWidth, initialHeight)
-
class CustomSession : AbstractSandboxedUiAdapter.AbstractSession() {
override val view = TextView(context)
@@ -349,9 +303,7 @@
view.text = "Test View"
}
}
-
val customSession = CustomSession()
-
class CustomUiAdapter : AbstractSandboxedUiAdapter() {
override fun openSession(
context: Context,
@@ -365,12 +317,9 @@
clientExecutor.execute { client.onSessionOpened(customSession) }
}
}
-
view.setAdapter(CustomUiAdapter())
addViewToLayout(waitToBeActive = true)
-
customSession.view.layout(0, 0, initialWidth * 2, initialHeight * 2)
-
assertThat(customSession.view.width).isEqualTo(initialWidth * 2)
assertThat(customSession.view.height).isEqualTo(initialHeight * 2)
assertThat(view.width).isEqualTo(initialWidth)
@@ -495,11 +444,9 @@
fun inputTokenIsCorrect() {
// Input token is only needed when provider can be located on a separate process.
assumeTrue(BackwardCompatUtil.canProviderBeRemote())
-
lateinit var layout: LinearLayout
val surfaceView = SurfaceView(context)
val surfaceViewLatch = CountDownLatch(1)
-
var token: IBinder? = null
surfaceView.addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
@@ -512,18 +459,15 @@
override fun onViewDetachedFromWindow(p0: View) {}
}
)
-
// Attach SurfaceView
activityScenarioRule.withActivity {
layout = findViewById(R.id.mainlayout)
layout.addView(surfaceView)
layout.removeView(surfaceView)
}
-
// Verify SurfaceView has a non-null token when attached.
assertThat(surfaceViewLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
assertThat(token).isNotNull()
-
// Verify that the UI adapter receives the same host token object when opening a session.
addViewToLayout()
testSandboxedUiAdapter.assertSessionOpened()
@@ -752,7 +696,6 @@
Runnable { view.removeAllViewsInLayout() },
Runnable { view.removeViewsInLayout(0, 0) }
)
-
removeChildRunnableArray.forEach { removeChildRunnable ->
val exception =
assertThrows(UnsupportedOperationException::class.java) {
@@ -807,4 +750,11 @@
assertThat(width).isEqualTo(expectedWidth)
assertThat(height).isEqualTo(expectedHeight)
}
+
+ companion object {
+ const val TIMEOUT = 1000.toLong()
+ // Longer timeout used for expensive operations like device rotation.
+ const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
+ const val SHORTEST_TIME_BETWEEN_SIGNALS_MS = 200
+ }
}
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt
index 5bfe8df..f9ebe61 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/TestSandboxedUiAdapter.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.privacysandbox.ui.client.test
import android.content.Context
@@ -32,20 +31,18 @@
class TestSandboxedUiAdapter(private val signalOptions: Set<String> = setOf("option")) :
AbstractSandboxedUiAdapter() {
-
var isSessionOpened = false
var internalClient: SandboxedUiAdapter.SessionClient? = null
var testSession: TestSession? = null
var isZOrderOnTop = true
var inputToken: IBinder? = null
-
// When set to true, the onSessionOpened callback will only be invoked when specified
// by the test. This is to test race conditions when the session is being loaded.
var delayOpenSessionCallback = false
-
private val openSessionLatch = CountDownLatch(1)
private val resizeLatch = CountDownLatch(1)
private val configChangedLatch = CountDownLatch(1)
+ private val sessionClosedLatch = CountDownLatch(1)
override fun openSession(
context: Context,
@@ -98,16 +95,21 @@
)
}
+ internal fun assertSessionNotClosed() {
+ Truth.assertThat(
+ sessionClosedLatch.await(SandboxedSdkViewTest.TIMEOUT, TimeUnit.MILLISECONDS)
+ )
+ .isFalse()
+ }
+
inner class TestSession(context: Context, override val signalOptions: Set<String>) :
SandboxedUiAdapter.Session {
-
var zOrderChangedLatch: CountDownLatch = CountDownLatch(1)
var shortestGapBetweenUiChangeEvents = Long.MAX_VALUE
private var notifyUiChangedLatch: CountDownLatch = CountDownLatch(1)
private var latestUiChange: Bundle = Bundle()
private var hasReceivedFirstUiChange = false
private var timeReceivedLastUiChange = SystemClock.elapsedRealtime()
-
override val view: View = View(context)
fun requestResize(width: Int, height: Int) {
@@ -127,7 +129,9 @@
configChangedLatch.countDown()
}
- override fun close() {}
+ override fun close() {
+ sessionClosedLatch.countDown()
+ }
override fun notifyUiChanged(uiContainerInfo: Bundle) {
if (hasReceivedFirstUiChange) {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
new file mode 100644
index 0000000..27b1299
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/UiLibComposeActivity.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 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.privacysandbox.ui.client.test
+
+import androidx.activity.ComponentActivity
+
+class UiLibComposeActivity : ComponentActivity() {}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkUi.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkUi.kt
new file mode 100644
index 0000000..29f873a
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkUi.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 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.privacysandbox.ui.client.view
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+
+/**
+ * Composable that can be used to remotely render UI from a SandboxedSdk to host app window.
+ *
+ * @param sandboxedUiAdapter an adapter that provides content from a SandboxedSdk to be displayed as
+ * part of a host app's window.
+ * @param modifier the [Modifier] to be applied to this SandboxedSdkUi.
+ * @param providerUiOnTop sets the Z-ordering of the SandboxedSdkUi surface, relative to its window.
+ * @param sandboxedSdkViewEventListener an event listener to the UI presentation.
+ */
+@Composable
+@Suppress("MissingJvmstatic")
+fun SandboxedSdkUi(
+ sandboxedUiAdapter: SandboxedUiAdapter,
+ modifier: Modifier = Modifier,
+ providerUiOnTop: Boolean = true,
+ sandboxedSdkViewEventListener: SandboxedSdkViewEventListener? = null
+) {
+ val delegatedListener =
+ remember {
+ object : SandboxedSdkViewEventListener {
+ var delegate by mutableStateOf(sandboxedSdkViewEventListener)
+
+ override fun onUiDisplayed() {
+ delegate?.onUiDisplayed()
+ }
+
+ override fun onUiError(error: Throwable) {
+ delegate?.onUiError(error)
+ }
+
+ override fun onUiClosed() {
+ delegate?.onUiClosed()
+ }
+ }
+ }
+ .apply { delegate = sandboxedSdkViewEventListener }
+ AndroidView(
+ modifier = modifier,
+ factory = { context ->
+ SandboxedSdkView(context).apply { setEventListener(delegatedListener) }
+ },
+ update = { view ->
+ view.setAdapter(sandboxedUiAdapter)
+ view.orderProviderUiAboveClientUi(providerUiOnTop)
+ }
+ )
+}
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index e0ab2c1..3805343 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -41,7 +41,6 @@
api(libs.kotlinStdlib)
api("androidx.paging:paging-common:3.3.2")
api(project(":room:room-runtime"))
- implementation(libs.atomicFu)
}
}
@@ -66,6 +65,9 @@
nativeMain {
dependsOn(jvmNativeMain)
+ dependencies {
+ implementation(libs.atomicFu)
+ }
}
androidInstrumentedTest {
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
index 4128526..66cbfa3 100644
--- a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
@@ -61,7 +61,7 @@
private val implementation = CommonLimitOffsetImpl(tables, this, ::convertRows)
public actual val itemCount: Int
- get() = implementation.itemCount.value
+ get() = implementation.itemCount.get()
override val jumpingSupported: Boolean
get() = true
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index edd5950..9c5783d4 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -24,11 +24,12 @@
import androidx.room.RoomDatabase
import androidx.room.RoomRawQuery
import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.AtomicInt
import androidx.room.paging.util.INITIAL_ITEM_COUNT
import androidx.room.paging.util.queryDatabase
import androidx.room.paging.util.queryItemCount
import androidx.room.useReaderConnection
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -70,9 +71,9 @@
private val db = pagingSource.db
private val sourceQuery = pagingSource.sourceQuery
- internal val itemCount = atomic(INITIAL_ITEM_COUNT)
+ internal val itemCount = AtomicInt(INITIAL_ITEM_COUNT)
- private val invalidationFlowStarted = atomic(false)
+ private val invalidationFlowStarted = AtomicBoolean(false)
private var invalidationFlowJob: Job? = null
init {
@@ -80,7 +81,7 @@
}
suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
- if (invalidationFlowStarted.compareAndSet(expect = false, update = true)) {
+ if (invalidationFlowStarted.compareAndSet(false, true)) {
invalidationFlowJob =
db.getCoroutineScope().launch {
db.invalidationTracker.createFlow(*tables, emitInitialState = false).collect {
@@ -92,7 +93,7 @@
}
}
- val tempCount = itemCount.value
+ val tempCount = itemCount.get()
// if itemCount is < 0, then it is initial load
return try {
if (tempCount == INITIAL_ITEM_COUNT) {
@@ -119,7 +120,7 @@
return db.useReaderConnection { connection ->
connection.withTransaction(SQLiteTransactionType.DEFERRED) {
val tempCount = queryItemCount(sourceQuery, db)
- itemCount.value = tempCount
+ itemCount.set(tempCount)
queryDatabase(
params = params,
sourceQuery = sourceQuery,
diff --git a/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt b/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
index 1f4235e..77f4bd2 100644
--- a/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
+++ b/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
@@ -41,7 +41,7 @@
private val implementation = CommonLimitOffsetImpl(tables, this, ::convertRows)
public actual val itemCount: Int
- get() = implementation.itemCount.value
+ get() = implementation.itemCount.get()
override val jumpingSupported: Boolean
get() = true
diff --git a/room/room-runtime/bcv/native/current.txt b/room/room-runtime/bcv/native/current.txt
index 984bd87..89d7a96 100644
--- a/room/room-runtime/bcv/native/current.txt
+++ b/room/room-runtime/bcv/native/current.txt
@@ -222,6 +222,24 @@
final object Companion // androidx.room/EntityUpsertAdapter.Companion|null[0]
}
+final class androidx.room.concurrent/AtomicBoolean { // androidx.room.concurrent/AtomicBoolean|null[0]
+ constructor <init>(kotlin/Boolean) // androidx.room.concurrent/AtomicBoolean.<init>|<init>(kotlin.Boolean){}[0]
+
+ final fun compareAndSet(kotlin/Boolean, kotlin/Boolean): kotlin/Boolean // androidx.room.concurrent/AtomicBoolean.compareAndSet|compareAndSet(kotlin.Boolean;kotlin.Boolean){}[0]
+ final fun get(): kotlin/Boolean // androidx.room.concurrent/AtomicBoolean.get|get(){}[0]
+}
+
+final class androidx.room.concurrent/AtomicInt { // androidx.room.concurrent/AtomicInt|null[0]
+ constructor <init>(kotlin/Int) // androidx.room.concurrent/AtomicInt.<init>|<init>(kotlin.Int){}[0]
+
+ final fun compareAndSet(kotlin/Int, kotlin/Int): kotlin/Boolean // androidx.room.concurrent/AtomicInt.compareAndSet|compareAndSet(kotlin.Int;kotlin.Int){}[0]
+ final fun decrementAndGet(): kotlin/Int // androidx.room.concurrent/AtomicInt.decrementAndGet|decrementAndGet(){}[0]
+ final fun get(): kotlin/Int // androidx.room.concurrent/AtomicInt.get|get(){}[0]
+ final fun getAndIncrement(): kotlin/Int // androidx.room.concurrent/AtomicInt.getAndIncrement|getAndIncrement(){}[0]
+ final fun incrementAndGet(): kotlin/Int // androidx.room.concurrent/AtomicInt.incrementAndGet|incrementAndGet(){}[0]
+ final fun set(kotlin/Int) // androidx.room.concurrent/AtomicInt.set|set(kotlin.Int){}[0]
+}
+
final class androidx.room.util/ByteArrayWrapper { // androidx.room.util/ByteArrayWrapper|null[0]
constructor <init>(kotlin/ByteArray) // androidx.room.util/ByteArrayWrapper.<init>|<init>(kotlin.ByteArray){}[0]
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index dc736fd..4fd3768 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -110,7 +110,6 @@
api("androidx.collection:collection:1.4.2")
api("androidx.annotation:annotation:1.8.1")
api(libs.kotlinCoroutinesCore)
- implementation(libs.atomicFu)
}
}
commonTest {
@@ -180,6 +179,7 @@
dependsOn(jvmNativeMain)
dependencies {
api(project(":sqlite:sqlite-framework"))
+ implementation(libs.atomicFu)
}
}
nativeTest {
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
index ab9a93b..f876d37 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
@@ -18,6 +18,7 @@
import androidx.kruth.assertThat
import androidx.room.Transactor
+import androidx.room.concurrent.AtomicInt
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.filters.LargeTest
@@ -27,7 +28,6 @@
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.InternalCoroutinesApi
@@ -130,7 +130,7 @@
/** A CoroutineDispatcher that dispatches every block into a new thread */
private class NewThreadDispatcher : CoroutineDispatcher() {
- private val idCounter = atomic(0)
+ private val idCounter = AtomicInt(0)
@OptIn(InternalCoroutinesApi::class)
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
index 04caeb7..9b5e5d8 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
@@ -21,12 +21,12 @@
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.room.InvalidationTracker.Observer
+import androidx.room.concurrent.ReentrantLock
+import androidx.room.concurrent.withLock
import androidx.room.support.AutoCloser
import androidx.sqlite.SQLiteConnection
import java.lang.ref.WeakReference
import java.util.concurrent.Callable
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.runBlocking
@@ -65,7 +65,7 @@
)
private val observerMap = mutableMapOf<Observer, ObserverWrapper>()
- private val observerMapLock = reentrantLock()
+ private val observerMapLock = ReentrantLock()
private var autoCloser: AutoCloser? = null
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index bbe94dc..c5e76f9 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -19,6 +19,8 @@
import androidx.annotation.RequiresApi
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.AtomicInt
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement
@@ -27,7 +29,6 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.collections.removeFirst as removeFirstKt
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancelAndJoin
@@ -346,12 +347,12 @@
fun refreshAndCloseDbWithSlowObserver() = runTest {
// Validates that a slow observer will finish notification after database closing
val invalidatedLatch = CountDownLatch(1)
- val invalidated = atomic(false)
+ val invalidated = AtomicBoolean(false)
tracker.addObserver(
object : InvalidationTracker.Observer("a") {
override fun onInvalidated(tables: Set<String>) {
invalidatedLatch.countDown()
- assertThat(invalidated.compareAndSet(expect = false, update = true)).isTrue()
+ assertThat(invalidated.compareAndSet(false, true)).isTrue()
runBlocking { delay(100) }
}
}
@@ -361,7 +362,7 @@
testScheduler.advanceUntilIdle()
invalidatedLatch.await()
roomDatabase.close()
- assertThat(invalidated.value).isTrue()
+ assertThat(invalidated.get()).isTrue()
}
@Test
@@ -473,7 +474,7 @@
@Test
fun weakObserver() = runTest {
- val invalidated = atomic(0)
+ val invalidated = AtomicInt(0)
var observer: InvalidationTracker.Observer? =
object : InvalidationTracker.Observer("a") {
override fun onInvalidated(tables: Set<String>) {
@@ -484,7 +485,7 @@
sqliteDriver.setInvalidatedTables(0)
tracker.awaitRefreshAsync()
- assertThat(invalidated.value).isEqualTo(1)
+ assertThat(invalidated.get()).isEqualTo(1)
// Attempt to perform garbage collection in a loop so that weak observer is discarded
// and it stops receiving invalidation notifications. If GC fails to collect the observer
@@ -511,7 +512,7 @@
sqliteDriver.setInvalidatedTables(0)
tracker.awaitRefreshAsync()
- assertThat(invalidated.value).isEqualTo(1)
+ assertThat(invalidated.get()).isEqualTo(1)
}
@Test
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
index 1997321..5730b60 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
@@ -18,16 +18,16 @@
import androidx.annotation.RestrictTo
import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.ReentrantLock
import androidx.room.concurrent.ifNotClosed
+import androidx.room.concurrent.withLock
import androidx.room.util.getCoroutineContext
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteException
import androidx.sqlite.execSQL
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmSuppressWildcards
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
@@ -162,7 +162,7 @@
* queue to be done asynchronously, this flag is used to control excessive scheduling of
* refreshes.
*/
- private val pendingRefresh = atomic(false)
+ private val pendingRefresh = AtomicBoolean(false)
/** Callback to allow or disallow [refreshInvalidation] from proceeding. */
internal var onAllowRefresh: () -> Boolean = { true }
@@ -484,7 +484,7 @@
*/
internal class ObservedTableStates(size: Int) {
- private val lock = reentrantLock()
+ private val lock = ReentrantLock()
// The number of observers per table
private val tableObserversCount = LongArray(size)
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Atomics.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Atomics.kt
new file mode 100644
index 0000000..04abb13
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Atomics.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2025 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:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.concurrent
+
+import androidx.annotation.RestrictTo
+
+expect class AtomicInt {
+ constructor(initialValue: Int)
+
+ fun get(): Int
+
+ fun set(value: Int)
+
+ fun compareAndSet(expect: Int, update: Int): Boolean
+
+ fun incrementAndGet(): Int
+
+ fun getAndIncrement(): Int
+
+ fun decrementAndGet(): Int
+}
+
+internal inline fun AtomicInt.loop(action: (Int) -> Unit): Nothing {
+ while (true) {
+ action(get())
+ }
+}
+
+expect class AtomicBoolean {
+ constructor(initialValue: Boolean)
+
+ fun get(): Boolean
+
+ fun compareAndSet(expect: Boolean, update: Boolean): Boolean
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
index 8bf4a24..e499315 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/CloseBarrier.kt
@@ -16,11 +16,6 @@
package androidx.room.concurrent
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.locks.SynchronizedObject
-import kotlinx.atomicfu.locks.synchronized
-import kotlinx.atomicfu.loop
-
/**
* A barrier that can be used to perform a cleanup action once, waiting for registered parties
* (blockers) to finish using the protected resource.
@@ -40,9 +35,10 @@
* blockers.
*/
internal class CloseBarrier(private val closeAction: () -> Unit) : SynchronizedObject() {
- private val blockers = atomic(0)
- private val closeInitiated = atomic(false)
- private val isClosed by closeInitiated
+ private val blockers = AtomicInt(0)
+ private val closeInitiated = AtomicBoolean(false)
+ private val isClosed: Boolean
+ get() = closeInitiated.get()
/**
* Blocks the [closeAction] from occurring.
@@ -72,7 +68,7 @@
internal fun unblock(): Unit =
synchronized(this) {
blockers.decrementAndGet()
- check(blockers.value >= 0) { "Unbalanced call to unblock() detected." }
+ check(blockers.get() >= 0) { "Unbalanced call to unblock() detected." }
}
/**
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
index de1c2e5..2d34dd7 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
@@ -16,11 +16,6 @@
package androidx.room.concurrent
-import kotlinx.atomicfu.locks.ReentrantLock
-import kotlinx.atomicfu.locks.SynchronizedObject
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.synchronized
-
/**
* An exclusive lock for in-process and multi-process synchronization.
*
@@ -59,7 +54,7 @@
private fun getThreadLock(key: String): ReentrantLock =
synchronized(this) {
- return threadLocksMap.getOrPut(key) { reentrantLock() }
+ return threadLocksMap.getOrPut(key) { ReentrantLock() }
}
private fun getFileLock(key: String) = FileLock(key)
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ReentrantLock.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ReentrantLock.kt
new file mode 100644
index 0000000..444ef7c
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ReentrantLock.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 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.room.concurrent
+
+internal expect class ReentrantLock() {
+ fun lock(): Unit
+
+ fun tryLock(): Boolean
+
+ fun unlock(): Unit
+}
+
+internal inline fun <T> ReentrantLock.withLock(block: () -> T): T {
+ lock()
+ try {
+ return block()
+ } finally {
+ unlock()
+ }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Synchronized.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Synchronized.kt
new file mode 100644
index 0000000..1515bdf
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/Synchronized.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 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.room.concurrent
+
+internal expect open class SynchronizedObject()
+
+internal expect inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
index 185e412..ae49a2e 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
@@ -19,6 +19,8 @@
import androidx.room.TransactionScope
import androidx.room.Transactor
import androidx.room.Transactor.SQLiteTransactionType
+import androidx.room.concurrent.AtomicBoolean
+import androidx.room.concurrent.AtomicInt
import androidx.room.concurrent.ThreadLocal
import androidx.room.concurrent.asContextElement
import androidx.room.concurrent.currentThreadId
@@ -35,7 +37,6 @@
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.time.Duration.Companion.seconds
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.Mutex
@@ -50,8 +51,9 @@
private val threadLocal = ThreadLocal<PooledConnectionImpl>()
- private val _isClosed = atomic(false)
- private val isClosed by _isClosed
+ private val _isClosed = AtomicBoolean(false)
+ private val isClosed: Boolean
+ get() = _isClosed.get()
// Amount of time to wait to acquire a connection before throwing, Android uses 30 seconds in
// its pool, so we do too here, but IDK if that is a good number. This timeout is unrelated to
@@ -195,7 +197,7 @@
}
private class Pool(val capacity: Int, val connectionFactory: () -> SQLiteConnection) {
- private val size = atomic(0)
+ private val size = AtomicInt(0)
private val connections = arrayOfNulls<ConnectionWithLock>(capacity)
private val channel =
Channel<ConnectionWithLock>(capacity = capacity, onUndeliveredElement = { recycle(it) })
@@ -211,7 +213,7 @@
}
private fun tryOpenNewConnection() {
- val currentSize = size.value
+ val currentSize = size.get()
if (currentSize >= capacity) {
// Capacity reached
return
@@ -322,8 +324,9 @@
) : Transactor, RawConnectionAccessor {
private val transactionStack = ArrayDeque<TransactionItem>()
- private val _isRecycled = atomic(false)
- private val isRecycled by _isRecycled
+ private val _isRecycled = AtomicBoolean(false)
+ private val isRecycled: Boolean
+ get() = _isRecycled.get()
override val rawConnection: SQLiteConnection
get() = delegate
diff --git a/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt b/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
index 75f64d1..3133b3a 100644
--- a/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
+++ b/room/room-runtime/src/commonTest/kotlin/androidx/room/concurrent/CloseBarrierTest.kt
@@ -19,7 +19,6 @@
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
import kotlin.test.Test
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@@ -34,9 +33,9 @@
@Test
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
fun oneBlocker() = runTest {
- val actionPerformed = atomic(false)
+ val actionPerformed = AtomicBoolean(false)
val closeBarrier = CloseBarrier {
- assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+ assertThat(actionPerformed.compareAndSet(false, true)).isTrue()
}
val jobLaunched = Mutex(locked = true)
@@ -52,14 +51,14 @@
// yield for launch and verify the close action has not been performed
yield()
- jobLaunched.withLock { assertThat(actionPerformed.value).isFalse() }
+ jobLaunched.withLock { assertThat(actionPerformed.get()).isFalse() }
// unblock the barrier, close job should complete
closeBarrier.unblock()
closeJob.join()
// verify action was performed
- assertThat(actionPerformed.value).isTrue()
+ assertThat(actionPerformed.get()).isTrue()
// verify a new block is not granted since the barrier is already close
assertThat(closeBarrier.block()).isFalse()
@@ -67,15 +66,15 @@
@Test
fun noBlockers() = runTest {
- val actionPerformed = atomic(false)
+ val actionPerformed = AtomicBoolean(false)
val closeBarrier = CloseBarrier {
- assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+ assertThat(actionPerformed.compareAndSet(false, true)).isTrue()
}
// Validate close action is performed immediately if there are no blockers
closeBarrier.close()
- assertThat(actionPerformed.value).isTrue()
+ assertThat(actionPerformed.get()).isTrue()
}
@Test
@@ -89,9 +88,9 @@
@Test
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
fun noStarvation() = runTest {
- val actionPerformed = atomic(false)
+ val actionPerformed = AtomicBoolean(false)
val closeBarrier = CloseBarrier {
- assertThat(actionPerformed.compareAndSet(expect = false, update = true)).isTrue()
+ assertThat(actionPerformed.compareAndSet(false, true)).isTrue()
}
val jobLaunched = Mutex(locked = true)
@@ -111,12 +110,12 @@
// yield for launch and verify the close action has not been performed in an attempt to
// get the block / unblock loop going
yield()
- jobLaunched.withLock { assertThat(actionPerformed.value).isFalse() }
+ jobLaunched.withLock { assertThat(actionPerformed.get()).isFalse() }
// initiate the close action, test should not deadlock (or timeout) meaning the barrier
// will not cause the caller to starve
closeBarrier.close()
blockerJob.join()
- assertThat(actionPerformed.value).isTrue()
+ assertThat(actionPerformed.get()).isTrue()
}
}
diff --git a/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt b/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
index cff6dbd..f86b7b6 100644
--- a/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
+++ b/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
@@ -19,6 +19,7 @@
import androidx.kruth.assertThat
import androidx.room.PooledConnection
import androidx.room.Transactor
+import androidx.room.concurrent.AtomicInt
import androidx.room.deferredTransaction
import androidx.room.exclusiveTransaction
import androidx.room.execSQL
@@ -34,7 +35,6 @@
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.fail
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -463,7 +463,7 @@
@Test
fun singleConnectionPool() = runTest {
val multiThreadContext = newFixedThreadPoolContext(2, "Test-Threads")
- val connectionsOpened = atomic(0)
+ val connectionsOpened = AtomicInt(0)
val actualDriver = setupDriver()
val driver =
object : SQLiteDriver by actualDriver {
@@ -487,12 +487,12 @@
jobs.joinAll()
pool.close()
multiThreadContext.close()
- assertThat(connectionsOpened.value).isEqualTo(1)
+ assertThat(connectionsOpened.get()).isEqualTo(1)
}
@Test
fun openOneConnectionWhenUsedSerially() = runTest {
- val connectionsOpened = atomic(0)
+ val connectionsOpened = AtomicInt(0)
val actualDriver = setupDriver()
val driver =
object : SQLiteDriver by actualDriver {
@@ -518,7 +518,7 @@
}
}
pool.close()
- assertThat(connectionsOpened.value).isEqualTo(1)
+ assertThat(connectionsOpened.get()).isEqualTo(1)
}
@Test
@@ -689,7 +689,7 @@
actual.close()
}
}
- val connectionArrCount = atomic(0)
+ val connectionArrCount = AtomicInt(0)
val connectionsArr = arrayOfNulls<CloseAwareConnection>(4)
val actualDriver = setupDriver()
val driver =
@@ -714,7 +714,7 @@
launch(multiThreadContext) { pool.useReaderConnection { barrier.withLock {} } }
jobs.add(job)
}
- while (connectionArrCount.value < 4) {
+ while (connectionArrCount.get() < 4) {
delay(100)
}
barrier.unlock()
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Atomics.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Atomics.jvmAndroid.kt
new file mode 100644
index 0000000..00255dc
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Atomics.jvmAndroid.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2025 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:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.concurrent
+
+import androidx.annotation.RestrictTo
+
+actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
+
+actual typealias AtomicBoolean = java.util.concurrent.atomic.AtomicBoolean
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/ReentrantLock.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/ReentrantLock.jvmAndroid.kt
new file mode 100644
index 0000000..dc8fdab
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/ReentrantLock.jvmAndroid.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 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.room.concurrent
+
+internal actual typealias ReentrantLock = java.util.concurrent.locks.ReentrantLock
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Synchronized.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Synchronized.jvmAndroid.kt
new file mode 100644
index 0000000..fae30d5
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/Synchronized.jvmAndroid.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2025 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.room.concurrent
+
+internal actual typealias SynchronizedObject = Any
+
+internal actual inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T =
+ kotlin.synchronized(lock, block)
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Atomics.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Atomics.native.kt
new file mode 100644
index 0000000..46b4012
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Atomics.native.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 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:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+
+package androidx.room.concurrent
+
+import androidx.annotation.RestrictTo
+import kotlin.concurrent.AtomicInt as KotlinAtomicInt
+
+actual class AtomicInt actual constructor(initialValue: Int) {
+ private val delegate: KotlinAtomicInt = KotlinAtomicInt(initialValue)
+
+ actual fun get(): Int = delegate.value
+
+ actual fun set(value: Int) {
+ delegate.value = value
+ }
+
+ actual fun compareAndSet(expect: Int, update: Int): Boolean =
+ delegate.compareAndSet(expect, update)
+
+ actual fun incrementAndGet(): Int = delegate.incrementAndGet()
+
+ actual fun getAndIncrement(): Int = delegate.getAndIncrement()
+
+ actual fun decrementAndGet(): Int = delegate.decrementAndGet()
+}
+
+actual class AtomicBoolean actual constructor(initialValue: Boolean) {
+ private val delegate: KotlinAtomicInt = KotlinAtomicInt(toInt(initialValue))
+
+ actual fun get(): Boolean = delegate.value == 1
+
+ actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean =
+ delegate.compareAndSet(toInt(expect), toInt(update))
+
+ private fun toInt(value: Boolean) = if (value) 1 else 0
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/ReentrantLock.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/ReentrantLock.native.kt
new file mode 100644
index 0000000..544fa01
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/ReentrantLock.native.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 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.room.concurrent
+
+internal actual typealias ReentrantLock = kotlinx.atomicfu.locks.SynchronizedObject
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Synchronized.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Synchronized.native.kt
new file mode 100644
index 0000000..7691045
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/Synchronized.native.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2025 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.room.concurrent
+
+internal actual typealias SynchronizedObject = kotlinx.atomicfu.locks.SynchronizedObject
+
+internal actual inline fun <T> synchronized(lock: SynchronizedObject, block: () -> T): T =
+ kotlinx.atomicfu.locks.synchronized(lock, block)
diff --git a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
index 8fd4b86..616baa6 100644
--- a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
+++ b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
@@ -18,6 +18,8 @@
package androidx.savedstate
+import android.os.Parcel
+import android.os.Parcelable
import androidx.annotation.Sampled
import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState
@@ -96,7 +98,6 @@
val uuid = decodeFromSavedState(UUIDSerializer(), uuidSavedState)
}
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") // The lint warning does not show up for external users.
@Sampled
fun savedStateSerializer() {
@Serializable
@@ -125,20 +126,36 @@
)
}
+private class MyJavaSerializable : java.io.Serializable
+
+private class MyJavaSerializableSerializer : JavaSerializableSerializer<MyJavaSerializable>()
+
@Sampled
fun serializableSerializer() {
@Serializable
data class MyModel(
- @Serializable(with = JavaSerializableSerializer::class)
- val serializable: java.io.Serializable
+ @Serializable(with = MyJavaSerializableSerializer::class)
+ val serializable: MyJavaSerializable
)
}
+private class MyParcelable : Parcelable {
+ override fun describeContents(): Int {
+ TODO("Not yet implemented")
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ TODO("Not yet implemented")
+ }
+}
+
+private class MyParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
@Sampled
fun parcelableSerializer() {
@Serializable
data class MyModel(
- @Serializable(with = ParcelableSerializer::class) val parcelable: android.os.Parcelable
+ @Serializable(with = MyParcelableSerializer::class) val parcelable: MyParcelable
)
}
@@ -172,13 +189,11 @@
fun charSequenceListSerializer() {
@Serializable
class MyModel(
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(with = CharSequenceListSerializer::class)
val charSequenceList: List<CharSequence>
)
}
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Sampled
fun parcelableListSerializer() {
@Serializable
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index a3e7083..8a63bfe 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -195,12 +195,12 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+ public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
ctor public CharSequenceSerializer();
- method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
- method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
- method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
- property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+ method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+ method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+ method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+ property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -211,7 +211,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
ctor public JavaSerializableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -235,7 +235,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
ctor public ParcelableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index eb7a636..0f65840 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -220,12 +220,12 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+ public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
ctor public CharSequenceSerializer();
- method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
- method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
- method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
- property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+ method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+ method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+ method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+ property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -236,7 +236,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
ctor public JavaSerializableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -260,7 +260,7 @@
property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
}
- public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+ public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
ctor public ParcelableSerializer();
method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 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.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+ val parcel =
+ Parcel.obtain().apply {
+ savedState.writeToParcel(this, 0)
+ setDataPosition(0)
+ }
+ return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
index f0dacc5..071f4c1 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
@@ -107,10 +107,10 @@
* @see androidx.savedstate.serialization.decodeFromSavedState
*/
@OptIn(ExperimentalSerializationApi::class)
-public open class CharSequenceSerializer<T : CharSequence> : KSerializer<T> {
- final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
+public class CharSequenceSerializer : KSerializer<CharSequence> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
- final override fun serialize(encoder: Encoder, value: T) {
+ override fun serialize(encoder: Encoder, value: CharSequence) {
require(encoder is SavedStateEncoder) {
encoderErrorMessage(descriptor.serialName, encoder)
}
@@ -118,17 +118,18 @@
}
@Suppress("UNCHECKED_CAST")
- final override fun deserialize(decoder: Decoder): T {
+ override fun deserialize(decoder: Decoder): CharSequence {
require(decoder is SavedStateDecoder) {
decoderErrorMessage(descriptor.serialName, decoder)
}
- return decoder.run { savedState.read { getCharSequence(key) as T } }
+ return decoder.run { savedState.read { getCharSequence(key) } }
}
}
/**
* A serializer for [java.io.Serializable]. This serializer uses [SavedState]'s API directly to
- * save/load a [java.io.Serializable].
+ * save/load a [java.io.Serializable]. You must extend this serializer for each of your
+ * [java.io.Serializable] subclasses.
*
* Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
* Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -138,7 +139,7 @@
* @see androidx.savedstate.serialization.decodeFromSavedState
*/
@OptIn(ExperimentalSerializationApi::class)
-public open class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
+public abstract class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("JavaSerializable")
final override fun serialize(encoder: Encoder, value: T) {
@@ -159,7 +160,7 @@
/**
* A serializer for [Parcelable]. This serializer uses [SavedState]'s API directly to save/load a
- * [Parcelable].
+ * [Parcelable]. You must extend this serializer for each of your [Parcelable] subclasses.
*
* Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
* Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -169,7 +170,7 @@
* @see androidx.savedstate.serialization.decodeFromSavedState
*/
@OptIn(ExperimentalSerializationApi::class)
-public open class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
+public abstract class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Parcelable")
final override fun serialize(encoder: Encoder, value: T) {
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
index 2c34ddb..743c3e4 100644
--- a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
@@ -26,7 +26,6 @@
import android.util.SizeF
import android.util.SparseArray
import androidx.core.os.bundleOf
-import androidx.core.util.forEach
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
import androidx.savedstate.SavedStateCodecTestUtils.encodeDecode
@@ -59,7 +58,6 @@
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
-import kotlinx.serialization.serializer
@ExperimentalSerializationApi
internal class SavedStateCodecAndroidTest : RobolectricTest() {
@@ -100,18 +98,7 @@
"SERIALIZER_TYPE_INCOMPATIBLE"
) // The lint warning does not show up for external users.
@Serializable
- class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as MyClass
- return s.read { contentDeepEquals(other.s) }
- }
-
- override fun hashCode(): Int {
- return s.read { contentDeepHashCode() }
- }
- }
+ class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle)
MyClass(
bundleOf(
"i" to 1,
@@ -120,19 +107,24 @@
"ss" to bundleOf("s" to "bar")
)
)
- .encodeDecode {
- assertThat(size()).isEqualTo(1)
- getSavedState("s").read {
- assertThat(size()).isEqualTo(4)
- assertThat(getInt("i")).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("foo")
- assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
- getSavedState("ss").read {
- assertThat(size()).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("bar")
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ assertThat(decoded.s.read { contentDeepEquals(original.s) }).isTrue()
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ getSavedState("s").read {
+ assertThat(size()).isEqualTo(4)
+ assertThat(getInt("i")).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("foo")
+ assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+ getSavedState("ss").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("bar")
+ }
}
}
- }
+ )
// Bundle at root.
val origin = bundleOf("i" to 3, "s" to "foo", "d" to 3.14)
@@ -180,7 +172,8 @@
@Serializable
data class SerializableContainer(
- @Serializable(with = JavaSerializableSerializer::class) val value: java.io.Serializable
+ @Serializable(with = CustomJavaSerializableSerializer::class)
+ val value: java.io.Serializable
)
val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
SerializableContainer(myJavaSerializable).encodeDecode {
@@ -191,7 +184,7 @@
@Serializable
data class ParcelableContainer(
- @Serializable(with = ParcelableSerializer::class) val value: Parcelable
+ @Serializable(with = CustomParcelableSerializer::class) val value: Parcelable
)
val myParcelable = MyParcelable(3, "foo", 3.14)
ParcelableContainer(myParcelable).encodeDecode {
@@ -257,19 +250,9 @@
error("VERSION.SDK_INT < Q")
}
+ @Suppress("ArrayInDataClass")
@Serializable
- data class CharSequenceArrayContainer(val value: Array<out CharSequence>) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as CharSequenceArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
+ data class CharSequenceArrayContainer(val value: Array<out CharSequence>)
assertThrows<SerializationException> {
CharSequenceArrayContainer(arrayOf("foo", "bar")).encodeDecode {}
}
@@ -281,20 +264,10 @@
@Test
fun concreteTypesInsteadOfInterfaceTypes() {
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
- @Serializable
- data class CharSequenceContainer(
- @Serializable(with = CharSequenceSerializer::class) val value: String
- )
- CharSequenceContainer("foo").encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequence("value")).isEqualTo("foo")
- }
-
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class SerializableContainer(
- @Serializable(with = JavaSerializableSerializer::class) val value: MyJavaSerializable
+ @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
+ val value: MyJavaSerializable
)
val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
SerializableContainer(myJavaSerializable).encodeDecode {
@@ -303,10 +276,9 @@
.isEqualTo(myJavaSerializable)
}
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class ParcelableContainer(
- @Serializable(with = ParcelableSerializer::class) val value: MyParcelable
+ @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
)
val myParcelable = MyParcelable(3, "foo", 3.14)
ParcelableContainer(myParcelable).encodeDecode {
@@ -315,10 +287,11 @@
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class IBinderContainer(
- @Serializable(with = IBinderSerializer::class) val value: Binder
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Serializable(with = IBinderSerializer::class)
+ val value: Binder
)
val binder = Binder("foo")
IBinderContainer(binder).encodeDecode {
@@ -333,47 +306,36 @@
@Test
fun collectionTypes() {
@Serializable
+ @Suppress("ArrayInDataClass")
data class CharSequenceArrayContainer(
@Serializable(with = CharSequenceArraySerializer::class)
val value: Array<out CharSequence>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as CharSequenceArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
- val myCharSequenceArray = arrayOf("foo", "bar")
- CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
- }
+ )
+ val myCharSequenceArray = arrayOf(StringBuilder("foo"), StringBuilder("bar"))
+ CharSequenceArrayContainer(myCharSequenceArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
+ }
+ )
@Serializable
+ @Suppress("ArrayInDataClass")
data class ParcelableArrayContainer(
@Serializable(with = ParcelableArraySerializer::class) val value: Array<out Parcelable>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as ParcelableArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
+ )
val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
- ParcelableArrayContainer(myParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
- }
+ ParcelableArrayContainer(myParcelableArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getParcelableArray<MyParcelable>("value"))
+ .isEqualTo(myParcelableArray)
+ }
+ )
@Serializable
data class CharSequenceListContainer(
@@ -406,76 +368,96 @@
append(1, MyParcelable(3, "foo", 3.14))
append(3, MyParcelable(4, "bar", 1.73))
}
- SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getSparseParcelableArray<Parcelable>("value"))
- .isEqualTo(mySparseParcelableArray)
- }
+ SparseParcelableArrayContainer(mySparseParcelableArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ decoded.value.contentEquals(original.value)
+ } else {
+ error("VERSION.SDK_INT < S")
+ }
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getSparseParcelableArray<Parcelable>("value"))
+ .isEqualTo(mySparseParcelableArray)
+ }
+ )
}
@Test
fun collectionTypesWithConcreteElement() {
+ @Suppress("ArrayInDataClass")
@Serializable
data class CharSequenceArrayContainer(
- @Serializable(with = CharSequenceArraySerializer::class) val value: Array<String>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as CharSequenceArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Serializable(with = CharSequenceArraySerializer::class)
+ val value: Array<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
+ )
+ val myCharSequenceArray = arrayOf<StringBuilder>(StringBuilder("foo"), StringBuilder("bar"))
+ // `Bundle.getCharSequenceArray()` returns a `CharSequence[]` and the actual element type
+ // is not being retained after parcel/unparcel so the plugin-generated serializer will
+ // get `ClassCastException` when trying to cast it back to `Array<StringBuilder>`.
+ assertThrows(ClassCastException::class) {
+ CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
}
}
- val myCharSequenceArray = arrayOf("foo", "bar")
- CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
- }
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ @Suppress("ArrayInDataClass")
@Serializable
data class ParcelableArrayContainer(
@Serializable(with = ParcelableArraySerializer::class)
// Here the serializer for the element is actually not used, but leaving it out leads
// to SERIALIZER_NOT_FOUND compile error.
- val value: Array<@Serializable(with = ParcelableSerializer::class) MyParcelable>
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- other as ParcelableArrayContainer
- return value.contentEquals(other.value)
- }
-
- override fun hashCode(): Int {
- return value.contentHashCode()
- }
- }
+ val value:
+ Array<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
+ )
val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
- ParcelableArrayContainer(myParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+ // Even though `Bundle` does retain the actual `Parcelable` type there's no way for us to
+ // specify this `Parcelable` element type for the array, so the restored array is still of
+ // type `Array<Parcelable>` and the plugin-generated serializer will get
+ // `ClassCastException` when trying to cast it back to `Array<MyParcelable>`.
+ assertThrows(ClassCastException::class) {
+ ParcelableArrayContainer(myParcelableArray).encodeDecode {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+ }
}
@Serializable
data class CharSequenceListContainer(
- @Serializable(with = CharSequenceListSerializer::class) val value: List<String>
+ @Serializable(with = CharSequenceListSerializer::class)
+ @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ val value: List<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
)
- val myCharSequenceList = arrayListOf("foo", "bar")
- CharSequenceListContainer(myCharSequenceList).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
- }
+ val myCharSequenceList = arrayListOf(StringBuilder("foo"), StringBuilder("bar"))
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+ CharSequenceListContainer(myCharSequenceList)
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ assertThat(original.value[0]::class).isEqualTo(StringBuilder::class)
+ // This is similar to the `CharSequenceArray` case where the element type of the
+ // restored List after parcel/unparcel is of `String` instead of
+ // `StringBuilder`. However, since the element type of Lists is erased no
+ // `CastCastException` is thrown when the plugin-generated serializer tried to
+ // assign the restored list back to `List<StringBuilder>`.
+ assertThat(decoded.value[0]::class).isEqualTo(String::class)
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
+ }
+ )
+
@Serializable
data class ParcelableListContainer(
+ // Unlike arrays this works as `List`s can be down-casted, e.g.
+ // a `List<Parcelable>` can be casted to `List<MyParcelable>`.
@Serializable(with = ParcelableListSerializer::class)
- val value: List<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+ val value:
+ List<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
)
val myParcelableList =
arrayListOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
@@ -484,57 +466,37 @@
assertThat(getParcelableList<MyParcelable>("value")).isEqualTo(myParcelableList)
}
- @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable
data class SparseParcelableArrayContainer(
+ // Unlike arrays this works as `SparseArray`s can be down-casted, e.g.
+ // a `SparseArray<Parcelable>` can be casted to `SparseArray<MyParcelable>`.
@Serializable(with = SparseParcelableArraySerializer::class)
- val value: SparseArray<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+ val value:
+ SparseArray<
+ @Serializable(with = MyParcelableAsParcelableSerializer::class)
+ MyParcelable
+ >
)
val mySparseParcelableArray =
SparseArray<MyParcelable>().apply {
append(1, MyParcelable(3, "foo", 3.14))
append(3, MyParcelable(4, "bar", 1.73))
}
- SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getSparseParcelableArray<Parcelable>("value"))
- .isEqualTo(mySparseParcelableArray)
- }
- }
-
- @Test
- fun concreteTypeSerializers() {
- // No need to suppress SERIALIZER_TYPE_INCOMPATIBLE with these serializers.
- @Serializable
- data class CharSequenceContainer(
- @Serializable(with = StringAsCharSequenceSerializer::class) val value: String
- )
- CharSequenceContainer("foo").encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getCharSequence("value")).isEqualTo("foo")
- }
-
- @Serializable
- data class SerializableContainer(
- @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
- val value: MyJavaSerializable
- )
- val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
- SerializableContainer(myJavaSerializable).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getJavaSerializable<MyJavaSerializable>("value"))
- .isEqualTo(myJavaSerializable)
- }
-
- @Serializable
- data class ParcelableContainer(
- @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
- )
- val myParcelable = MyParcelable(3, "foo", 3.14)
- ParcelableContainer(myParcelable).encodeDecode {
- assertThat(size()).isEqualTo(1)
- assertThat(getParcelable<MyParcelable>("value")).isEqualTo(myParcelable)
- }
+ SparseParcelableArrayContainer(mySparseParcelableArray)
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ assertThat(decoded.value.contentEquals(original.value))
+ } else {
+ error("VERSION.SDK_INT < S")
+ }
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getSparseParcelableArray<Parcelable>("value"))
+ .isEqualTo(mySparseParcelableArray)
+ }
+ )
}
}
@@ -610,45 +572,11 @@
}
}
-private object CharArrayAsStringSerializer : KSerializer<Array<Char>> {
- private val delegateSerializer = serializer<String>()
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("Array<Char>", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): Array<Char> {
- val s = decoder.decodeSerializableValue(delegateSerializer)
- val result = Array(s.length) { s[it] }
- return result
- }
-
- override fun serialize(encoder: Encoder, value: Array<Char>) {
- val charArray = CharArray(value.size)
- value.forEachIndexed { index, c -> charArray[index] = c }
- encoder.encodeSerializableValue(delegateSerializer, String(charArray))
- }
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-private object SparseStringArrayAsMapSerializer : KSerializer<SparseArray<String>> {
- private val delegateSerializer = serializer<Map<Int, String>>()
- override val descriptor = SerialDescriptor("SparseArray<String>", delegateSerializer.descriptor)
-
- override fun deserialize(decoder: Decoder): SparseArray<String> {
- val m = decoder.decodeSerializableValue(delegateSerializer)
- val result = SparseArray<String>()
- m.forEach { (k, v) -> result.append(k, v) }
- return result
- }
-
- override fun serialize(encoder: Encoder, value: SparseArray<String>) {
- val map = buildMap { value.forEach { k, v -> put(k, v) } }
- encoder.encodeSerializableValue(delegateSerializer, map)
- }
-}
-
-private class StringAsCharSequenceSerializer : CharSequenceSerializer<String>()
-
private class MyJavaSerializableAsJavaSerializableSerializer :
JavaSerializableSerializer<MyJavaSerializable>()
private class MyParcelableAsParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
+private class CustomJavaSerializableSerializer : JavaSerializableSerializer<java.io.Serializable>()
+
+private class CustomParcelableSerializer : ParcelableSerializer<Parcelable>()
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 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.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+ val parcel =
+ Parcel.obtain().apply {
+ savedState.writeToParcel(this, 0)
+ setDataPosition(0)
+ }
+ return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
index 65ccd1f..3f06dff 100644
--- a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
@@ -22,6 +22,7 @@
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.EmptySerializersModule
@@ -73,9 +74,31 @@
private var index = 0
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
- if (index == savedState.read { size() }) return CompositeDecoder.DECODE_DONE
- key = descriptor.getElementName(index)
- return index++
+ val size =
+ if (descriptor.kind == StructureKind.LIST || descriptor.kind == StructureKind.MAP) {
+ // Use the number of elements encoded for collections.
+ savedState.read { size() }
+ } else {
+ // We may skip elements when encoding so if we used `size()`
+ // here we may miss some fields.
+ descriptor.elementsCount
+ }
+ fun hasDefaultValueDefined(index: Int) = descriptor.isElementOptional(index)
+ fun presentInEncoding(index: Int) =
+ savedState.read {
+ val key = descriptor.getElementName(index)
+ contains(key)
+ }
+ // Skip elements omitted from encoding (those assigned with its default values).
+ while (index < size && hasDefaultValueDefined(index) && !presentInEncoding(index)) {
+ index++
+ }
+ if (index < size) {
+ key = descriptor.getElementName(index)
+ return index++
+ } else {
+ return CompositeDecoder.DECODE_DONE
+ }
}
override fun decodeBoolean(): Boolean = savedState.read { getBoolean(key) }
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
index 86adf0b..88c33e4 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
@@ -401,15 +401,19 @@
@Test
fun sealedClasses() {
- Node.Add(Node.Operand(3), Node.Operand(5)).encodeDecode {
+ // Should use base type for encoding/decoding.
+ Node.Add(Node.Operand(3), Node.Operand(5)).encodeDecode<Node> {
assertThat(size()).isEqualTo(2)
- getSavedState("lhs").read {
- assertThat(size()).isEqualTo(1)
- assertThat(getInt("value")).isEqualTo(3)
- }
- getSavedState("rhs").read {
- assertThat(size()).isEqualTo(1)
- assertThat(getInt("value")).isEqualTo(5)
+ assertThat(getString("type")).isEqualTo("androidx.savedstate.Node.Add")
+ getSavedState("value").read {
+ getSavedState("lhs").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getInt("value")).isEqualTo(3)
+ }
+ getSavedState("rhs").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getInt("value")).isEqualTo(5)
+ }
}
}
}
@@ -425,12 +429,24 @@
}
// Nullable with default value.
- @Serializable data class B(val s: String? = "foo")
- B().encodeDecode()
- B(s = "bar").encodeDecode {
+ @Serializable data class B(val s: String? = "foo", val i: Int)
+ B(i = 3).encodeDecode {
assertThat(size()).isEqualTo(1)
+ assertThat(getInt("i")).isEqualTo(3)
+ }
+ B(s = null, i = 3).encodeDecode {
+ assertThat(size()).isEqualTo(2)
+ assertThat(isNull("s")).isTrue()
+ }
+ B(s = "bar", i = 3).encodeDecode {
+ assertThat(size()).isEqualTo(2)
assertThat(getString("s")).isEqualTo("bar")
}
+ // The value of `s` is the same as its default value so it's omitted from encoding.
+ B(s = "foo", i = 3).encodeDecode {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getInt("i")).isEqualTo(3)
+ }
// Nullable without default value
@Serializable data class C(val s: String?)
@@ -450,6 +466,22 @@
assertThat(getInt("i")).isEqualTo(5)
assertThat(getString("s")).isEqualTo("foo")
}
+
+ // Nullable with null as default value.
+ @Serializable data class E(val s: String? = null)
+ // Even though we encode `null`s in general as we don't encode default values
+ // nothing is encoded.
+ E().encodeDecode()
+
+ // Nullable in parent
+ G(i = 3).encodeDecode<F> {
+ assertThat(size()).isEqualTo(2)
+ assertThat(getString("type")).isEqualTo("androidx.savedstate.G")
+ getSavedState("value").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getInt("i")).isEqualTo(3)
+ }
+ }
}
@Test
@@ -464,19 +496,24 @@
putSavedState("ss", savedState { putString("s", "bar") })
}
)
- .encodeDecode {
- assertThat(size()).isEqualTo(1)
- getSavedState("s").read {
- assertThat(size()).isEqualTo(4)
- assertThat(getInt("i")).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("foo")
- assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
- getSavedState("ss").read {
- assertThat(size()).isEqualTo(1)
- assertThat(getString("s")).isEqualTo("bar")
+ .encodeDecode(
+ checkDecoded = { decoded, original ->
+ assertThat(decoded.s.read { contentDeepEquals(original.s) })
+ },
+ checkEncoded = {
+ assertThat(size()).isEqualTo(1)
+ getSavedState("s").read {
+ assertThat(size()).isEqualTo(4)
+ assertThat(getInt("i")).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("foo")
+ assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+ getSavedState("ss").read {
+ assertThat(size()).isEqualTo(1)
+ assertThat(getString("s")).isEqualTo("bar")
+ }
}
}
- }
+ )
val origin = savedState {
putInt("i", 1)
@@ -563,6 +600,7 @@
private typealias MyNestedTypeAlias = MyTypeAliasToInt
+@Serializable
private sealed class Node {
@Serializable data class Add(val lhs: Operand, val rhs: Operand) : Node()
@@ -605,3 +643,10 @@
return MyColor(array[0], array[1], array[2])
}
}
+
+@Serializable
+private sealed class F {
+ val s: String? = null
+}
+
+@Serializable private data class G(val i: Int) : F()
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
index e9ac161..67930cd 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
@@ -23,16 +23,30 @@
import kotlinx.serialization.serializer
internal object SavedStateCodecTestUtils {
+ /* Test the following steps: 1. encode `T` to a `SavedState`, 2. parcelize it to a `Parcel`,
+ * 3. un-parcelize it back to a `SavedState`, and 4. decode it back to a `T`. Step 2 and 3
+ * are only performed on Android. Here's the whole process:
+ *
+ * (A)Serializable -1-> (B)SavedState -2-> (C)Parcel -3-> (D)SavedState -4-> (E)Serializable
+ *
+ * `checkEncoded` can be used to check the content of "B", and `checkDecoded` can be
+ * used to compare the instances of "E" and "A".
+ */
inline fun <reified T : Any> T.encodeDecode(
serializer: KSerializer<T> = serializer<T>(),
- checkContent: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
+ checkDecoded: (T, T) -> Unit = { decoded, original ->
+ assertThat(decoded).isEqualTo(original)
+ },
+ checkEncoded: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
) {
- assertThat(
- decodeFromSavedState(
- serializer,
- encodeToSavedState(serializer, this).apply { read { checkContent() } }
- )
- )
- .isEqualTo(this)
+ val encoded = encodeToSavedState(serializer, this)
+ encoded.read { checkEncoded() }
+
+ val restored = platformEncodeDecode(encoded)
+
+ val decoded = decodeFromSavedState(serializer, restored)
+ checkDecoded(decoded, this)
}
}
+
+expect fun platformEncodeDecode(savedState: SavedState): SavedState
diff --git a/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
new file mode 100644
index 0000000..023701a8
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 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.savedstate
+
+// No parceling in non-Android platforms.
+actual fun platformEncodeDecode(savedState: SavedState): SavedState = savedState
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 8426c60..efb8ff6 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId = "androidx.wear.compose.integration.demos"
minSdk = 25
- versionCode = 63
- versionName = "1.63"
+ versionCode = 64
+ versionName = "1.64"
}
buildTypes {
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 8942afc..eae2d58 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -204,17 +204,6 @@
method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
}
- public final class EdgeButtonStyle {
- field public static final androidx.wear.protolayout.material3.EdgeButtonStyle.Companion Companion;
- field public static final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
- field public static final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
- }
-
- public static final class EdgeButtonStyle.Companion {
- property public final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
- property public final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
- }
-
public final class GraphicDataCardStyle {
field public static final androidx.wear.protolayout.material3.GraphicDataCardStyle.Companion Companion;
}
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 8942afc..eae2d58 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -204,17 +204,6 @@
method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
}
- public final class EdgeButtonStyle {
- field public static final androidx.wear.protolayout.material3.EdgeButtonStyle.Companion Companion;
- field public static final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
- field public static final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
- }
-
- public static final class EdgeButtonStyle.Companion {
- property public final androidx.wear.protolayout.material3.EdgeButtonStyle DEFAULT;
- property public final androidx.wear.protolayout.material3.EdgeButtonStyle TOP_ALIGN;
- }
-
public final class GraphicDataCardStyle {
field public static final androidx.wear.protolayout.material3.GraphicDataCardStyle.Companion Companion;
}
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index 6b02da5..4537676 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -83,14 +83,17 @@
testCases["primarylayout_edgebuttonfilled_buttongroup_iconoverride_golden$goldenSuffix"] =
materialScope(
ApplicationProvider.getApplicationContext(),
- deviceParameters,
+ // renderer version 1.302 has no asymmetrical corner support, so edgebutton will use
+ // its fallback style
+ deviceParameters.copy(VersionInfo.Builder().setMajor(1).setMinor(302).build()),
allowDynamicTheme = false
) {
primaryLayoutWithOverrideIcon(
mainSlot = {
text(
- text = "Text in the main slot that overflows".layoutString,
- color = colorScheme.secondary
+ text = "Overflow main text and fallback edge button".layoutString,
+ color = colorScheme.secondary,
+ maxLines = 3
)
},
bottomSlot = {
@@ -476,11 +479,23 @@
testCases["primarylayout_circularprogressindicators_fallback__golden$NORMAL_SCALE_SUFFIX"] =
materialScope(
ApplicationProvider.getApplicationContext(),
+ // renderer with version 1.302 has no DashedArcLine or asymmetrical corners support
deviceConfiguration =
- deviceParameters.copy(VersionInfo.Builder().setMajor(1).setMinor(402).build()),
+ deviceParameters.copy(VersionInfo.Builder().setMajor(1).setMinor(302).build()),
allowDynamicTheme = false
) {
- primaryLayout(mainSlot = { progressIndicatorGroup() })
+ primaryLayout(
+ mainSlot = { progressIndicatorGroup() },
+ bottomSlot = {
+ iconEdgeButton(
+ onClick = clickable,
+ iconContent = { icon(ICON_ID) },
+ modifier =
+ LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
+ colors = filledTonalButtonColors()
+ )
+ }
+ )
}
return collectTestCases(testCases)
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
index e503797..5c438b7 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
@@ -27,8 +27,10 @@
import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
+import androidx.wear.protolayout.material3.EdgeButtonDefaults.CONTAINER_HEIGHT_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.HORIZONTAL_MARGIN_PERCENT_LARGE
import androidx.wear.protolayout.material3.EdgeButtonDefaults.HORIZONTAL_MARGIN_PERCENT_SMALL
@@ -37,8 +39,16 @@
import androidx.wear.protolayout.material3.EdgeButtonDefaults.TEXT_SIDE_PADDING_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.TEXT_TOP_PADDING_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.TOP_CORNER_RADIUS
-import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.DEFAULT
-import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TOP_ALIGN
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.BOTTOM_MARGIN_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.CORNER_RADIUS_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.EDGE_BUTTON_HEIGHT_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.ICON_SIDE_PADDING_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.ICON_SIZE_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.TEXT_SIDE_PADDING_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.ICON
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.ICON_FALLBACK
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TEXT
+import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TEXT_FALLBACK
import androidx.wear.protolayout.modifiers.LayoutModifier
import androidx.wear.protolayout.modifiers.background
import androidx.wear.protolayout.modifiers.clickable
@@ -50,6 +60,7 @@
import androidx.wear.protolayout.modifiers.semanticsRole
import androidx.wear.protolayout.modifiers.tag
import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.dp
/**
* ProtoLayout Material3 component edge button that offers a single slot to take an icon or similar
@@ -82,14 +93,22 @@
modifier: LayoutModifier = LayoutModifier,
colors: ButtonColors = filledButtonColors(),
iconContent: (MaterialScope.() -> LayoutElement)
-): LayoutElement =
- edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = DEFAULT) {
+): LayoutElement {
+ val style =
+ if (deviceConfiguration.rendererSchemaVersion.hasAsymmetricalCornersSupport()) {
+ ICON
+ } else {
+ ICON_FALLBACK
+ }
+
+ return edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = style) {
withStyle(
defaultIconStyle =
- IconStyle(size = ICON_SIZE_DP.toDp(), tintColor = colors.iconColor)
+ IconStyle(size = style.iconSizeDp.dp, tintColor = colors.iconColor)
)
.iconContent()
}
+}
/**
* ProtoLayout Material3 component edge button that offers a single slot to take a text or similar
@@ -123,7 +142,17 @@
colors: ButtonColors = filledButtonColors(),
labelContent: (MaterialScope.() -> LayoutElement)
): LayoutElement =
- edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = TOP_ALIGN) {
+ edgeButton(
+ onClick = onClick,
+ modifier = modifier,
+ colors = colors,
+ style =
+ if (deviceConfiguration.rendererSchemaVersion.hasAsymmetricalCornersSupport()) {
+ TEXT
+ } else {
+ TEXT_FALLBACK
+ }
+ ) {
withStyle(
defaultTextElementStyle =
TextElementStyle(
@@ -156,8 +185,8 @@
* @param modifier Modifiers to set to this element. It's highly recommended to set a content
* description using [contentDescription].
* @param style The style used for the inner content, specifying how the content should be aligned.
- * It is recommended to use [EdgeButtonStyle.TOP_ALIGN] for long, wide content. If not set,
- * defaults to [EdgeButtonStyle.DEFAULT] which center-aligns the content.
+ * It is recommended to use [EdgeButtonStyle.TEXT] for long, wide content. If not set, defaults to
+ * [EdgeButtonStyle.ICON] which center-aligns the content.
* @param content The inner content to be put inside of this edge button.
* @sample androidx.wear.protolayout.material3.samples.edgeButtonSampleIcon
*/
@@ -166,7 +195,7 @@
onClick: Clickable,
colors: ButtonColors,
modifier: LayoutModifier = LayoutModifier,
- style: EdgeButtonStyle = DEFAULT,
+ style: EdgeButtonStyle = ICON,
content: MaterialScope.() -> LayoutElement
): LayoutElement {
val containerWidth = deviceConfiguration.screenWidthDp.toDp()
@@ -175,50 +204,83 @@
else HORIZONTAL_MARGIN_PERCENT_SMALL
val edgeButtonWidth: Float =
(100f - 2f * horizontalMarginPercent) * deviceConfiguration.screenWidthDp / 100f
- val bottomCornerRadiusX = edgeButtonWidth / 2f
- val bottomCornerRadiusY = EDGE_BUTTON_HEIGHT_DP - TOP_CORNER_RADIUS
var mod =
(LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier)
.clickable(onClick)
.background(colors.containerColor)
- .clip(TOP_CORNER_RADIUS)
- .clipBottomLeft(bottomCornerRadiusX, bottomCornerRadiusY)
- .clipBottomRight(bottomCornerRadiusX, bottomCornerRadiusY)
+ .clip(style.topCornerRadiusDp)
+
+ if (deviceConfiguration.rendererSchemaVersion.hasAsymmetricalCornersSupport()) {
+ val bottomCornerRadiusX = edgeButtonWidth / 2f
+ val bottomCornerRadiusY = style.buttonHeightDp - style.topCornerRadiusDp
+ mod =
+ mod.clipBottomLeft(bottomCornerRadiusX, bottomCornerRadiusY)
+ .clipBottomRight(bottomCornerRadiusX, bottomCornerRadiusY)
+ }
style.padding?.let { mod = mod.padding(it) }
- val button = Box.Builder().setHeight(EDGE_BUTTON_HEIGHT_DP.toDp()).setWidth(dp(edgeButtonWidth))
- button
- .setVerticalAlignment(style.verticalAlignment)
- .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
- .addContent(content())
+ val button =
+ Box.Builder()
+ .setHeight(style.buttonHeightDp.dp)
+ .setWidth(dp(edgeButtonWidth))
+ .setVerticalAlignment(style.verticalAlignment)
+ .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
+ .addContent(content())
+ .setModifiers(mod.toProtoLayoutModifiers())
+ .build()
return Box.Builder()
- .setHeight((EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP).toDp())
+ .setHeight(CONTAINER_HEIGHT_DP.dp)
.setWidth(containerWidth)
- .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_TOP)
+ .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM)
.setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
- .addContent(button.setModifiers(mod.toProtoLayoutModifiers()).build())
- .setModifiers(LayoutModifier.tag(METADATA_TAG).toProtoLayoutModifiers())
+ .addContent(button)
+ .setModifiers(
+ LayoutModifier.tag(METADATA_TAG)
+ .padding(padding(bottom = style.bottomMarginDp.toFloat()))
+ .toProtoLayoutModifiers()
+ )
.build()
}
-/** Provides style values for edge button component. */
-public class EdgeButtonStyle
-private constructor(
+/**
+ * Provides style values for edge button component.
+ *
+ * An [edgeButton] has a wrapper container with the screen width, and fixed height of
+ * [CONTAINER_HEIGHT_DP].
+ *
+ * The visible button box has height of [buttonHeightDp], it is centered horizontally in side the
+ * wrapper container with [HORIZONTAL_MARGIN_PERCENT_SMALL] or [HORIZONTAL_MARGIN_PERCENT_LARGE]
+ * depending on the screen size. It is then horizontally aligned to the bottom with bottom margin of
+ * [bottomMarginDp].
+ *
+ * The visible button box has its top two corners clipped with the [topCornerRadiusDp], while its
+ * bottom two corners will be clipped fully both horizontally and vertically to achieving the edge
+ * hugging shape. In the fallback implementation, without asymmetrical corners support, the
+ * [topCornerRadiusDp] is applied to all four corners.
+ *
+ * The content (icon or text) of the button is located inside the visible button box with [padding],
+ * horizontally centered and vertically aligned with the given [verticalAlignment].
+ */
+internal class EdgeButtonStyle
+internal constructor(
@VerticalAlignment internal val verticalAlignment: Int = VERTICAL_ALIGN_CENTER,
- internal val padding: Padding? = null
+ internal val padding: Padding? = null,
+ @Dimension(DP) internal val buttonHeightDp: Float = EDGE_BUTTON_HEIGHT_DP,
+ @Dimension(DP) internal val bottomMarginDp: Float = BOTTOM_MARGIN_DP,
+ @Dimension(DP) internal val iconSizeDp: Float = ICON_SIZE_DP,
+ @Dimension(DP) internal val topCornerRadiusDp: Float = TOP_CORNER_RADIUS
) {
- public companion object {
+ internal companion object {
/**
- * Style variation for having content of the edge button anchored to the top.
+ * Style variation for having text content with edge hugging shape.
*
- * This should be used for text-like content, or the content that is wide, to accommodate
- * for more space.
+ * The text is vertically aligned to top with [TEXT_TOP_PADDING_DP] to to accommodate for
+ * more horizontal space.
*/
- @JvmField
- public val TOP_ALIGN: EdgeButtonStyle =
+ internal val TEXT: EdgeButtonStyle =
EdgeButtonStyle(
verticalAlignment = LayoutElementBuilders.VERTICAL_ALIGN_TOP,
padding =
@@ -230,28 +292,101 @@
)
/**
- * Default style variation for having content of the edge button center aligned.
+ * Style variation for having icon content with edge hugging shape.
*
- * This should be used for icon-like or small, round content that doesn't occupy a lot of
- * space.
+ * The icon is centered in the visible button box with the size of [ICON_SIZE_DP].
*/
- @JvmField public val DEFAULT: EdgeButtonStyle = EdgeButtonStyle()
+ internal val ICON: EdgeButtonStyle = EdgeButtonStyle()
+
+ /**
+ * Style variation for fallback implementation with text content, when there is no
+ * asymmetrical corners support.
+ *
+ * Without the edge hugging shape, the [topCornerRadius] value is a full cornered value and
+ * is applied to all four corners. To avoid being clipped by the screen edge, the visible
+ * button box is pushed upwards with a bigger bottom margin of [BOTTOM_MARGIN_FALLBACK_DP].
+ * Also the box height shrinks to [EDGE_BUTTON_HEIGHT_FALLBACK_DP].
+ *
+ * Its text content is center placed with a increased horizontal padding of
+ * [TEXT_SIDE_PADDING_FALLBACK_DP].
+ */
+ internal val TEXT_FALLBACK: EdgeButtonStyle =
+ EdgeButtonStyle(
+ verticalAlignment = VERTICAL_ALIGN_CENTER,
+ padding =
+ padding(
+ start = TEXT_SIDE_PADDING_FALLBACK_DP,
+ end = TEXT_SIDE_PADDING_FALLBACK_DP,
+ ),
+ buttonHeightDp = EDGE_BUTTON_HEIGHT_FALLBACK_DP,
+ topCornerRadiusDp = CORNER_RADIUS_FALLBACK_DP,
+ bottomMarginDp = BOTTOM_MARGIN_FALLBACK_DP
+ )
+
+ /**
+ * Style variation for fallback implementation with icon content, when there is no
+ * asymmetrical corners support.
+ *
+ * Without the edge hugging shape, the [topCornerRadius] value is a full cornered value and
+ * is applied to all four corners. To avoid being clipped by the screen, the visible button
+ * box is pushed upwards with a bigger bottom margin of [BOTTOM_MARGIN_FALLBACK_DP]. Also
+ * the box height shrinks to [EDGE_BUTTON_HEIGHT_FALLBACK_DP]
+ *
+ * Its icon content center placed with increased horizontal padding
+ * [ICON_SIDE_PADDING_FALLBACK_DP]. Also, the icon size is also increased to
+ * [ICON_SIZE_FALLBACK_DP].
+ */
+ internal val ICON_FALLBACK: EdgeButtonStyle =
+ EdgeButtonStyle(
+ verticalAlignment = VERTICAL_ALIGN_CENTER,
+ padding =
+ padding(
+ start = ICON_SIDE_PADDING_FALLBACK_DP,
+ end = ICON_SIDE_PADDING_FALLBACK_DP,
+ ),
+ buttonHeightDp = EDGE_BUTTON_HEIGHT_FALLBACK_DP,
+ topCornerRadiusDp = CORNER_RADIUS_FALLBACK_DP,
+ bottomMarginDp = BOTTOM_MARGIN_FALLBACK_DP,
+ iconSizeDp = ICON_SIZE_FALLBACK_DP
+ )
}
}
internal object EdgeButtonDefaults {
- @Dimension(DP) internal const val TOP_CORNER_RADIUS: Float = 17f
+ @Dimension(DP) internal const val TOP_CORNER_RADIUS = 17f
/** The horizontal margin used for width of the EdgeButton, below the 225dp breakpoint. */
- internal const val HORIZONTAL_MARGIN_PERCENT_SMALL: Float = 24f
+ internal const val HORIZONTAL_MARGIN_PERCENT_SMALL = 24f
/** The horizontal margin used for width of the EdgeButton, above the 225dp breakpoint. */
- internal const val HORIZONTAL_MARGIN_PERCENT_LARGE: Float = 26f
- internal const val BOTTOM_MARGIN_DP: Int = 3
- internal const val EDGE_BUTTON_HEIGHT_DP: Int = 46
- internal const val METADATA_TAG: String = "EB"
- internal const val ICON_SIZE_DP = 24
- internal const val TEXT_TOP_PADDING_DP = 12f
- internal const val TEXT_SIDE_PADDING_DP = 8f
+ internal const val HORIZONTAL_MARGIN_PERCENT_LARGE = 26f
+ @Dimension(DP) internal const val BOTTOM_MARGIN_DP = 3f
+ @Dimension(DP) internal const val EDGE_BUTTON_HEIGHT_DP = 46f
+ @Dimension(DP) internal const val CONTAINER_HEIGHT_DP = EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP
+ internal const val METADATA_TAG = "EB"
+ @Dimension(DP) internal const val ICON_SIZE_DP = 24f
+ @Dimension(DP) internal const val TEXT_TOP_PADDING_DP = 12f
+ @Dimension(DP) internal const val TEXT_SIDE_PADDING_DP = 8f
+}
+
+/**
+ * This object provides constants and styles of the fallback layout for [iconEdgeButton] and
+ * [textEdgeButton] when the renderer version is lower than 1.3.3 where asymmetrical corners support
+ * is not available.
+ */
+internal object EdgeButtonFallbackDefaults {
+ @Dimension(DP) internal const val ICON_SIZE_FALLBACK_DP = 26f
+ @Dimension(DP) internal const val EDGE_BUTTON_HEIGHT_FALLBACK_DP = 40f
+ @Dimension(DP) internal const val BOTTOM_MARGIN_FALLBACK_DP = 7f
+ @Dimension(DP)
+ internal const val CORNER_RADIUS_FALLBACK_DP = EDGE_BUTTON_HEIGHT_FALLBACK_DP / 2f
+ @Dimension(DP) internal const val TEXT_SIDE_PADDING_FALLBACK_DP = 14f
+ @Dimension(DP) internal const val ICON_SIDE_PADDING_FALLBACK_DP = 20f
}
internal fun LayoutElement.isSlotEdgeButton(): Boolean =
this is Box && METADATA_TAG == this.modifiers?.metadata?.toTagName()
+
+/**
+ * Checks whether the renderer has support for asymmetrical corners, which is added in version
+ * 1.303.
+ */
+private fun VersionInfo.hasAsymmetricalCornersSupport() = major > 1 || (major == 1 && minor >= 303)
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
index c8c40e5..15636ae 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
@@ -25,8 +25,11 @@
import androidx.wear.protolayout.LayoutElementBuilders.Image
import androidx.wear.protolayout.expression.AppDataKey
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
-import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
+import androidx.wear.protolayout.material3.EdgeButtonDefaults.CONTAINER_HEIGHT_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.EDGE_BUTTON_HEIGHT_FALLBACK_DP
+import androidx.wear.protolayout.material3.EdgeButtonFallbackDefaults.ICON_SIZE_FALLBACK_DP
import androidx.wear.protolayout.modifiers.LayoutModifier
import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
@@ -41,6 +44,7 @@
import androidx.wear.protolayout.testing.isClickable
import androidx.wear.protolayout.types.LayoutString
import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.dp
import androidx.wear.protolayout.types.layoutString
import org.junit.Test
import org.junit.runner.RunWith
@@ -54,14 +58,14 @@
LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
.onRoot()
.assert(hasWidth(DEVICE_CONFIGURATION.screenWidthDp.toDp()))
- .assert(hasHeight((EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP).toDp()))
+ .assert(hasHeight(CONTAINER_HEIGHT_DP.dp))
}
@Test
fun visibleHeight() {
LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
.onElement(isClickable())
- .assert(hasHeight(EDGE_BUTTON_HEIGHT_DP.toDp()))
+ .assert(hasHeight(EDGE_BUTTON_HEIGHT_DP.dp))
}
@Test
@@ -163,6 +167,29 @@
.assert(hasColor(COLOR_SCHEME.onPrimary.staticArgb))
}
+ @Test
+ fun containerFallbackSize() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON_FALLBACK)
+ .onRoot()
+ .assert(hasWidth(DEVICE_CONFIGURATION.screenWidthDp.toDp()))
+ .assert(hasHeight(CONTAINER_HEIGHT_DP.dp))
+ }
+
+ @Test
+ fun visibleFallbackHeight() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON_FALLBACK)
+ .onElement(isClickable())
+ .assert(hasHeight(EDGE_BUTTON_HEIGHT_FALLBACK_DP.dp))
+ }
+
+ @Test
+ fun iconFallbackSize() {
+ LayoutElementAssertionsProvider(ICON_EDGE_BUTTON_FALLBACK)
+ .onElement(hasImage(RES_ID))
+ .assert(hasWidth(ICON_SIZE_FALLBACK_DP.dp))
+ .assert(hasHeight(ICON_SIZE_FALLBACK_DP.dp))
+ }
+
companion object {
private val CONTEXT = getApplicationContext() as Context
private val COLOR_SCHEME = ColorScheme()
@@ -171,6 +198,14 @@
DeviceParametersBuilders.DeviceParameters.Builder()
.setScreenWidthDp(192)
.setScreenHeightDp(192)
+ .setRendererSchemaVersion(VersionInfo.Builder().setMajor(99).setMinor(999).build())
+ .build()
+
+ private val DEVICE_CONFIGURATION_WITH_OLD_RENDERER =
+ DeviceParametersBuilders.DeviceParameters.Builder()
+ .setScreenWidthDp(192)
+ .setScreenHeightDp(192)
+ .setRendererSchemaVersion(VersionInfo.Builder().setMajor(1).setMinor(302).build())
.build()
private val CLICKABLE =
@@ -188,5 +223,18 @@
icon(RES_ID)
}
}
+ private val ICON_EDGE_BUTTON_FALLBACK =
+ materialScope(
+ CONTEXT,
+ DEVICE_CONFIGURATION_WITH_OLD_RENDERER,
+ allowDynamicTheme = false
+ ) {
+ iconEdgeButton(
+ onClick = CLICKABLE,
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ ) {
+ icon(RES_ID)
+ }
+ }
}
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
index 7e04e8a..edfbff0 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
@@ -155,6 +155,9 @@
//
// If not set, the animation will play on load.</setter>
androidx.wear.protolayout.expression.proto.DynamicFloat progress = 2;
+
+ // The trigger to start the animation.
+ Trigger start_trigger = 3;
}
// An image resource, which can be used by layouts. This holds multiple
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 21886f5..da4e766 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1309,12 +1309,14 @@
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public static final class ResourceBuilders.AndroidLottieResourceByResId {
method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getProgress();
method @RawRes public int getRawResourceId();
+ method public androidx.wear.protolayout.TriggerBuilders.Trigger? getStartTrigger();
}
public static final class ResourceBuilders.AndroidLottieResourceByResId.Builder {
ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public ResourceBuilders.AndroidLottieResourceByResId.Builder(@RawRes int);
method public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setProgress(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setStartTrigger(androidx.wear.protolayout.TriggerBuilders.Trigger);
}
@SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class ResourceBuilders.AndroidSeekableAnimatedImageResourceByResId {
@@ -1434,6 +1436,24 @@
public final class TriggerBuilders {
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnConditionMetTrigger(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnLoadTrigger();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleOnceTrigger();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleTrigger();
+ }
+
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleOnceTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+ }
+
+ public static final class TriggerBuilders.OnVisibleOnceTrigger.Builder {
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleOnceTrigger.Builder();
+ method public androidx.wear.protolayout.TriggerBuilders.OnVisibleOnceTrigger build();
+ }
+
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+ }
+
+ public static final class TriggerBuilders.OnVisibleTrigger.Builder {
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleTrigger.Builder();
+ method public androidx.wear.protolayout.TriggerBuilders.OnVisibleTrigger build();
}
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface TriggerBuilders.Trigger {
@@ -1527,6 +1547,10 @@
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
}
+ public final class BorderKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier border(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float width, androidx.wear.protolayout.types.LayoutColor color);
+ }
+
public final class ClickableKt {
method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
@@ -1557,6 +1581,10 @@
method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
}
+ public final class OpacityKt {
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier opacity(androidx.wear.protolayout.modifiers.LayoutModifier, @FloatRange(from=0.0, to=1.0) float staticValue, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? dynamicValue);
+ }
+
public final class PaddingKt {
method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
@@ -1572,6 +1600,10 @@
method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
}
+ public final class VisibilityKt {
+ method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier visibility(androidx.wear.protolayout.modifiers.LayoutModifier, boolean staticVisibility, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool? dynamicVisibility);
+ }
+
}
package androidx.wear.protolayout.types {
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 21886f5..da4e766 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1309,12 +1309,14 @@
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public static final class ResourceBuilders.AndroidLottieResourceByResId {
method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getProgress();
method @RawRes public int getRawResourceId();
+ method public androidx.wear.protolayout.TriggerBuilders.Trigger? getStartTrigger();
}
public static final class ResourceBuilders.AndroidLottieResourceByResId.Builder {
ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public ResourceBuilders.AndroidLottieResourceByResId.Builder(@RawRes int);
method public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setProgress(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setStartTrigger(androidx.wear.protolayout.TriggerBuilders.Trigger);
}
@SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class ResourceBuilders.AndroidSeekableAnimatedImageResourceByResId {
@@ -1434,6 +1436,24 @@
public final class TriggerBuilders {
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnConditionMetTrigger(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnLoadTrigger();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleOnceTrigger();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleTrigger();
+ }
+
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleOnceTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+ }
+
+ public static final class TriggerBuilders.OnVisibleOnceTrigger.Builder {
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleOnceTrigger.Builder();
+ method public androidx.wear.protolayout.TriggerBuilders.OnVisibleOnceTrigger build();
+ }
+
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+ }
+
+ public static final class TriggerBuilders.OnVisibleTrigger.Builder {
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleTrigger.Builder();
+ method public androidx.wear.protolayout.TriggerBuilders.OnVisibleTrigger build();
}
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface TriggerBuilders.Trigger {
@@ -1527,6 +1547,10 @@
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
}
+ public final class BorderKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier border(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float width, androidx.wear.protolayout.types.LayoutColor color);
+ }
+
public final class ClickableKt {
method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
@@ -1557,6 +1581,10 @@
method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
}
+ public final class OpacityKt {
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier opacity(androidx.wear.protolayout.modifiers.LayoutModifier, @FloatRange(from=0.0, to=1.0) float staticValue, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? dynamicValue);
+ }
+
public final class PaddingKt {
method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
@@ -1572,6 +1600,10 @@
method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
}
+ public final class VisibilityKt {
+ method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier visibility(androidx.wear.protolayout.modifiers.LayoutModifier, boolean staticVisibility, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool? dynamicVisibility);
+ }
+
}
package androidx.wear.protolayout.types {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
index 0fff0a2..a51c996 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
@@ -520,6 +520,15 @@
}
}
+ /** Gets the trigger to start the animation. */
+ public @Nullable Trigger getStartTrigger() {
+ if (mImpl.hasStartTrigger()) {
+ return TriggerBuilders.triggerFromProto(mImpl.getStartTrigger());
+ } else {
+ return null;
+ }
+ }
+
/** Creates a new wrapper instance from the proto. */
@RestrictTo(Scope.LIBRARY_GROUP)
public static @NonNull AndroidLottieResourceByResId fromProto(
@@ -540,6 +549,8 @@
+ getRawResourceId()
+ ", progress="
+ getProgress()
+ + ", startTrigger="
+ + getStartTrigger()
+ "}";
}
@@ -590,6 +601,13 @@
return this;
}
+ /** Sets the trigger to start the animation. */
+ @RequiresSchemaVersion(major = 1, minor = 500)
+ public @NonNull Builder setStartTrigger(@NonNull Trigger startTrigger) {
+ mImpl.setStartTrigger(startTrigger.toTriggerProto());
+ return this;
+ }
+
/** Builds an instance from accumulated values. */
public @NonNull AndroidLottieResourceByResId build() {
return AndroidLottieResourceByResId.fromProto(mImpl.build());
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
index 1a96d0e..d05a53c 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
@@ -48,6 +48,160 @@
return new OnConditionMetTrigger.Builder().setCondition(dynamicBool).build();
}
+ /**
+ * Creates a {@link Trigger} that fires *every time* the layout becomes visible.
+ *
+ * <p>As opposed to {@link #createOnLoadTrigger()}, this will wait until layout is fully visible
+ * before firing a trigger.
+ */
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ public static @NonNull Trigger createOnVisibleTrigger() {
+ return new OnVisibleTrigger.Builder().build();
+ }
+
+ /**
+ * Creates a {@link Trigger} that fires the first time that layout becomes visible.
+ *
+ * <p>As opposed to {@link #createOnVisibleTrigger()}, this will only be fired the first time
+ * that the layout becomes visible.
+ */
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ public static @NonNull Trigger createOnVisibleOnceTrigger() {
+ return new OnVisibleOnceTrigger.Builder().build();
+ }
+
+ /** Triggers when the layout visibility state turns from invisible to fully visible. */
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ public static final class OnVisibleTrigger implements Trigger {
+ private final TriggerProto.OnVisibleTrigger mImpl;
+ private final @Nullable Fingerprint mFingerprint;
+
+ OnVisibleTrigger(TriggerProto.OnVisibleTrigger impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public @Nullable Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ /** Creates a new wrapper instance from the proto. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static @NonNull OnVisibleTrigger fromProto(
+ TriggerProto.@NonNull OnVisibleTrigger proto, @Nullable Fingerprint fingerprint) {
+ return new OnVisibleTrigger(proto, fingerprint);
+ }
+
+ static @NonNull OnVisibleTrigger fromProto(TriggerProto.@NonNull OnVisibleTrigger proto) {
+ return fromProto(proto, null);
+ }
+
+ /** Returns the internal proto instance. */
+ TriggerProto.@NonNull OnVisibleTrigger toProto() {
+ return mImpl;
+ }
+
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public TriggerProto.@NonNull Trigger toTriggerProto() {
+ return TriggerProto.Trigger.newBuilder().setOnVisibleTrigger(mImpl).build();
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "OnVisibleTrigger";
+ }
+
+ /** Builder for {@link OnVisibleTrigger}. */
+ @SuppressWarnings("HiddenSuperclass")
+ public static final class Builder implements Trigger.Builder {
+ private final TriggerProto.OnVisibleTrigger.Builder mImpl =
+ TriggerProto.OnVisibleTrigger.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(1416366796);
+
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ public Builder() {}
+
+ /** Builds an instance from accumulated values. */
+ @Override
+ public @NonNull OnVisibleTrigger build() {
+ return new OnVisibleTrigger(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
+ * Triggers only once when the layout visibility state turns from invisible to fully visible for
+ * the first time.
+ */
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ public static final class OnVisibleOnceTrigger implements Trigger {
+ private final TriggerProto.OnVisibleOnceTrigger mImpl;
+ private final @Nullable Fingerprint mFingerprint;
+
+ OnVisibleOnceTrigger(
+ TriggerProto.OnVisibleOnceTrigger impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public @Nullable Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ /** Creates a new wrapper instance from the proto. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static @NonNull OnVisibleOnceTrigger fromProto(
+ TriggerProto.@NonNull OnVisibleOnceTrigger proto,
+ @Nullable Fingerprint fingerprint) {
+ return new OnVisibleOnceTrigger(proto, fingerprint);
+ }
+
+ static @NonNull OnVisibleOnceTrigger fromProto(
+ TriggerProto.@NonNull OnVisibleOnceTrigger proto) {
+ return fromProto(proto, null);
+ }
+
+ /** Returns the internal proto instance. */
+ TriggerProto.@NonNull OnVisibleOnceTrigger toProto() {
+ return mImpl;
+ }
+
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public TriggerProto.@NonNull Trigger toTriggerProto() {
+ return TriggerProto.Trigger.newBuilder().setOnVisibleOnceTrigger(mImpl).build();
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "OnVisibleOnceTrigger";
+ }
+
+ /** Builder for {@link OnVisibleOnceTrigger}. */
+ @SuppressWarnings("HiddenSuperclass")
+ public static final class Builder implements Trigger.Builder {
+ private final TriggerProto.OnVisibleOnceTrigger.Builder mImpl =
+ TriggerProto.OnVisibleOnceTrigger.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1661457257);
+
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ public Builder() {}
+
+ /** Builds an instance from accumulated values. */
+ @Override
+ public @NonNull OnVisibleOnceTrigger build() {
+ return new OnVisibleOnceTrigger(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
/** Triggers immediately when the layout is loaded / reloaded. */
@RequiresSchemaVersion(major = 1, minor = 200)
static final class OnLoadTrigger implements Trigger {
@@ -225,6 +379,12 @@
@RestrictTo(Scope.LIBRARY_GROUP)
public static @NonNull Trigger triggerFromProto(
TriggerProto.@NonNull Trigger proto, @Nullable Fingerprint fingerprint) {
+ if (proto.hasOnVisibleTrigger()) {
+ return OnVisibleTrigger.fromProto(proto.getOnVisibleTrigger(), fingerprint);
+ }
+ if (proto.hasOnVisibleOnceTrigger()) {
+ return OnVisibleOnceTrigger.fromProto(proto.getOnVisibleOnceTrigger(), fingerprint);
+ }
if (proto.hasOnLoadTrigger()) {
return OnLoadTrigger.fromProto(proto.getOnLoadTrigger(), fingerprint);
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt
new file mode 100644
index 0000000..d3fca75
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 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.wear.protolayout.modifiers
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ModifiersBuilders.Border
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.dp
+
+/**
+ * Adds a modifier to apply a border around an element.
+ *
+ * @param width The width of the border, in `DP`.
+ * @param color The color of the border.
+ */
+fun LayoutModifier.border(@Dimension(DP) width: Float, color: LayoutColor): LayoutModifier =
+ this then BaseBorderElement(width, color)
+
+internal class BaseBorderElement(@Dimension(DP) val width: Float, val color: LayoutColor) :
+ LayoutModifier.Element {
+ fun foldIn(initial: Border.Builder?): Border.Builder =
+ (initial ?: Border.Builder()).setWidth(width.dp).setColor(color.prop)
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
index 0b5531a..aee3599 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
@@ -16,15 +16,23 @@
package androidx.wear.protolayout.modifiers
+import android.annotation.SuppressLint
+import androidx.annotation.OptIn
import androidx.wear.protolayout.ModifiersBuilders
import androidx.wear.protolayout.ModifiersBuilders.Background
+import androidx.wear.protolayout.ModifiersBuilders.Border
import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.ModifiersBuilders.Semantics
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.TypeBuilders.FloatProp
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
/** Creates a [ModifiersBuilders.Modifiers] from a [LayoutModifier]. */
+@SuppressLint("ProtoLayoutMinSchema")
+@OptIn(ProtoLayoutExperimental::class)
fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers {
var semantics: Semantics.Builder? = null
var background: Background.Builder? = null
@@ -32,6 +40,9 @@
var clickable: Clickable.Builder? = null
var padding: Padding.Builder? = null
var metadata: ElementMetadata.Builder? = null
+ var border: Border.Builder? = null
+ var visible: BoolProp.Builder? = null
+ var opacity: FloatProp.Builder? = null
this.foldIn(Unit) { _, e ->
when (e) {
@@ -41,6 +52,9 @@
is BaseClickableElement -> clickable = e.foldIn(clickable)
is BasePaddingElement -> padding = e.foldIn(padding)
is BaseMetadataElement -> metadata = e.foldIn(metadata)
+ is BaseBorderElement -> border = e.foldIn(border)
+ is BaseVisibilityElement -> visible = e.foldIn(visible)
+ is BaseOpacityElement -> opacity = e.foldIn(opacity)
}
}
@@ -53,6 +67,9 @@
clickable?.let { setClickable(it.build()) }
padding?.let { setPadding(it.build()) }
metadata?.let { setMetadata(it.build()) }
+ border?.let { setBorder(it.build()) }
+ visible?.let { setVisible(it.build()) }
+ opacity?.let { setOpacity(it.build()) }
}
.build()
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt
new file mode 100644
index 0000000..eeafb8f9
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 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.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.annotation.FloatRange
+import androidx.wear.protolayout.TypeBuilders.FloatProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+
+/**
+ * Adds a modifier to specify the opacity of the element with a value from 0 to 1, where 0 means the
+ * element is completely transparent and 1 means the element is completely opaque.
+ *
+ * @param staticValue The static value for opacity. This value will be used if [dynamicValue] is
+ * null, or if can't be resolved.
+ * @param dynamicValue The dynamic value for opacity. This can be used to change the opacity of the
+ * element dynamically (without changing the layout definition). To create a smooth transition for
+ * the dynamic change, you can use one of [DynamicFloat.animate] methods.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.opacity(
+ @FloatRange(from = 0.0, to = 1.0) staticValue: Float,
+ dynamicValue: DynamicFloat? = null
+): LayoutModifier = this then BaseOpacityElement(staticValue, dynamicValue)
+
+@RequiresSchemaVersion(major = 1, minor = 400)
+internal class BaseOpacityElement(val staticValue: Float, val dynamicValue: DynamicFloat? = null) :
+ LayoutModifier.Element {
+ @SuppressLint("ProtoLayoutMinSchema")
+ fun foldIn(initial: FloatProp.Builder?): FloatProp.Builder =
+ (initial ?: FloatProp.Builder(staticValue)).apply {
+ dynamicValue?.let { setDynamicValue(it) }
+ }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt
new file mode 100644
index 0000000..b7eabd7
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2025 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.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+
+/**
+ * Adds a modifier to specify the visibility of the element. A hidden element still consume space in
+ * the layout, but will not render any contents, nor will any of its children render any contents.
+ *
+ * Note that hidden elements won't receive input events.
+ *
+ * @param staticVisibility The static value for visibility. This value will be used if
+ * [dynamicVisibility] is null, or if can't be resolved.
+ * @param dynamicVisibility The dynamic value for visibility. This can be used to change the
+ * visibility of the element dynamically (without changing the layout definition).
+ */
+@RequiresSchemaVersion(major = 1, minor = 300)
+@ProtoLayoutExperimental
+fun LayoutModifier.visibility(
+ staticVisibility: Boolean,
+ dynamicVisibility: DynamicBool? = null
+): LayoutModifier = this then BaseVisibilityElement(staticVisibility, dynamicVisibility)
+
+@RequiresSchemaVersion(major = 1, minor = 300)
+internal class BaseVisibilityElement(
+ val visibility: Boolean,
+ val dynamicVisibility: DynamicBool? = null
+) : LayoutModifier.Element {
+ @SuppressLint("ProtoLayoutMinSchema")
+ fun foldIn(initial: BoolProp.Builder?): BoolProp.Builder =
+ (initial ?: BoolProp.Builder(visibility)).apply {
+ dynamicVisibility?.let { setDynamicValue(it) }
+ }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
index 3591875..c0256c3 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
@@ -74,9 +74,25 @@
.setProgress(DynamicBuilders.DynamicFloat.from(new AppDataKey<>(stateKey)))
.build();
- ResourceProto.AndroidLottieResourceByResId avdProto = lottieResource.toProto();
+ ResourceProto.AndroidLottieResourceByResId lottieProto = lottieResource.toProto();
- assertThat(avdProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
- assertThat(avdProto.getProgress().getStateSource().getSourceKey()).isEqualTo(stateKey);
+ assertThat(lottieProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
+ assertThat(lottieProto.getProgress().getStateSource().getSourceKey()).isEqualTo(stateKey);
+ }
+
+ @Test
+ public void lottieAnimation_hasTrigger() {
+ ResourceBuilders.AndroidLottieResourceByResId lottieResource =
+ new ResourceBuilders.AndroidLottieResourceByResId.Builder(RESOURCE_ID)
+ .setStartTrigger(TriggerBuilders.createOnVisibleTrigger())
+ .build();
+
+ ResourceProto.AndroidLottieResourceByResId lottieProto = lottieResource.toProto();
+
+ assertThat(lottieProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
+ assertThat(lottieProto.hasStartTrigger()).isTrue();
+ assertThat(lottieProto.getStartTrigger().hasOnVisibleTrigger()).isTrue();
+ assertThat(lottieProto.getStartTrigger().hasOnVisibleOnceTrigger()).isFalse();
+ assertThat(lottieProto.getStartTrigger().hasOnLoadTrigger()).isFalse();
}
}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
index 80f8b5e..ca402dc 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
@@ -24,6 +24,7 @@
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
import androidx.wear.protolayout.expression.AppDataKey
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
@@ -241,6 +242,26 @@
assertThat(modifiers.metadata?.tagData).isEqualTo(METADATA_BYTE_ARRAY)
}
+ @Test
+ fun border_toModifier() {
+ val modifier =
+ LayoutModifier.border(width = WIDTH_DP, color = COLOR).toProtoLayoutModifiers()
+
+ assertThat(modifier.border?.width?.value).isEqualTo(WIDTH_DP)
+ assertThat(modifier.border?.color?.argb).isEqualTo(COLOR.prop.argb)
+ }
+
+ @Test
+ fun visibility_toModifier() {
+ val modifier =
+ LayoutModifier.visibility(staticVisibility = false, dynamicVisibility = DYNAMIC_BOOL)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifier.isVisible.value).isEqualTo(false)
+ assertThat(modifier.isVisible.dynamicValue?.toDynamicBoolProto())
+ .isEqualTo(DYNAMIC_BOOL.toDynamicBoolProto())
+ }
+
companion object {
const val STATIC_CONTENT_DESCRIPTION = "content desc"
val DYNAMIC_CONTENT_DESCRIPTION = DynamicString.constant("dynamic content")
@@ -255,5 +276,7 @@
const val PADDING_ALL = 5f
const val METADATA = "metadata"
val METADATA_BYTE_ARRAY = METADATA.toByteArray()
+ const val WIDTH_DP = 5f
+ val DYNAMIC_BOOL = DynamicBool.constant(true)
}
}
diff --git a/wear/wear-phone-interactions/api/1.1.0-beta01.txt b/wear/wear-phone-interactions/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..22d6f21
--- /dev/null
+++ b/wear/wear-phone-interactions/api/1.1.0-beta01.txt
@@ -0,0 +1,170 @@
+// Signature format: 4.0
+package androidx.wear.phone.interactions {
+
+ public final class PhoneTypeHelper {
+ method public static int getPhoneDeviceType(android.content.Context context);
+ field public static final androidx.wear.phone.interactions.PhoneTypeHelper.Companion Companion;
+ field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
+ field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
+ field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+ field public static final int DEVICE_TYPE_NONE = 4; // 0x4
+ field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
+ }
+
+ public static final class PhoneTypeHelper.Companion {
+ method public int getPhoneDeviceType(android.content.Context context);
+ property public static final int DEVICE_TYPE_ANDROID;
+ property public static final int DEVICE_TYPE_ERROR;
+ property public static final int DEVICE_TYPE_IOS;
+ property public static final int DEVICE_TYPE_NONE;
+ property public static final int DEVICE_TYPE_UNKNOWN;
+ }
+
+}
+
+package androidx.wear.phone.interactions.authentication {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
+ ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
+ method public String getValue();
+ property public final String value;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
+ ctor public CodeVerifier();
+ ctor public CodeVerifier(optional int byteLength);
+ ctor public CodeVerifier(String value);
+ method public String getValue();
+ property public final String value;
+ }
+
+ public final class OAuthRequest {
+ method public String getPackageName();
+ method public String getRedirectUrl();
+ method public android.net.Uri getRequestUrl();
+ property public final String packageName;
+ property public final String redirectUrl;
+ property public final android.net.Uri requestUrl;
+ field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
+ field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
+ field public static final String WEAR_REDIRECT_URL_PREFIX_CN = "https://wear.googleapis-cn.com/3p_auth/";
+ }
+
+ public static final class OAuthRequest.Builder {
+ ctor public OAuthRequest.Builder(android.content.Context context);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.wear.phone.interactions.authentication.OAuthRequest build();
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setAuthProviderUrl(android.net.Uri authProviderUrl);
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setClientId(String clientId);
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setCodeChallenge(androidx.wear.phone.interactions.authentication.CodeChallenge codeChallenge);
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setRedirectUrl(android.net.Uri redirectUrl);
+ }
+
+ public static final class OAuthRequest.Companion {
+ property public static final String WEAR_REDIRECT_URL_PREFIX;
+ property public static final String WEAR_REDIRECT_URL_PREFIX_CN;
+ }
+
+ public final class OAuthResponse {
+ method public int getErrorCode();
+ method public android.net.Uri? getResponseUrl();
+ property public final int errorCode;
+ property public final android.net.Uri? responseUrl;
+ }
+
+ public static final class OAuthResponse.Builder {
+ ctor public OAuthResponse.Builder();
+ method public androidx.wear.phone.interactions.authentication.OAuthResponse build();
+ method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setErrorCode(int errorCode);
+ method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setResponseUrl(android.net.Uri responseUrl);
+ }
+
+ public final class RemoteAuthClient implements java.lang.AutoCloseable {
+ method @UiThread public void close();
+ method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+ method protected void finalize();
+ method public kotlinx.coroutines.flow.Flow<java.lang.Integer> getAvailabilityStatus();
+ method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+ property public final kotlinx.coroutines.flow.Flow<java.lang.Integer> availabilityStatus;
+ field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
+ field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+ field public static final int NO_ERROR = -1; // 0xffffffff
+ field public static final int STATUS_AVAILABLE = 3; // 0x3
+ field public static final int STATUS_TEMPORARILY_UNAVAILABLE = 2; // 0x2
+ field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
+ public abstract static class RemoteAuthClient.Callback {
+ ctor public RemoteAuthClient.Callback();
+ method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, int errorCode);
+ method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
+ }
+
+ public static final class RemoteAuthClient.Companion {
+ method public androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+ property public static final int ERROR_PHONE_UNAVAILABLE;
+ property public static final int ERROR_UNSUPPORTED;
+ property public static final int NO_ERROR;
+ property public static final int STATUS_AVAILABLE;
+ property public static final int STATUS_TEMPORARILY_UNAVAILABLE;
+ property public static final int STATUS_UNAVAILABLE;
+ property public static final int STATUS_UNKNOWN;
+ }
+
+ public interface RemoteAuthRequestHandler {
+ method public boolean isAuthSupported();
+ method public void sendAuthRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+ }
+
+ public abstract class RemoteAuthService extends android.app.Service {
+ ctor public RemoteAuthService();
+ method protected final android.os.IBinder onBind(android.content.Intent intent, androidx.wear.phone.interactions.authentication.RemoteAuthRequestHandler remoteAuthRequestHandler);
+ method public static final void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+ method protected boolean verifyPackageName(android.content.Context context, String? requestPackageName);
+ field public static final androidx.wear.phone.interactions.authentication.RemoteAuthService.Companion Companion;
+ }
+
+ public static final class RemoteAuthService.Companion {
+ method public void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+ }
+
+}
+
+package androidx.wear.phone.interactions.notifications {
+
+ public final class BridgingConfig {
+ method public java.util.Set<java.lang.String>? getExcludedTags();
+ method public boolean isBridgingEnabled();
+ property public final java.util.Set<java.lang.String>? excludedTags;
+ property public final boolean isBridgingEnabled;
+ }
+
+ public static final class BridgingConfig.Builder {
+ ctor public BridgingConfig.Builder(android.content.Context context, boolean isBridgingEnabled);
+ method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTag(String tag);
+ method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTags(java.util.Collection<java.lang.String> tags);
+ method public androidx.wear.phone.interactions.notifications.BridgingConfig build();
+ }
+
+ public fun interface BridgingConfigurationHandler {
+ method public void applyBridgingConfiguration(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+ }
+
+ public final class BridgingManager {
+ method public static androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+ method public void setConfig(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+ field public static final androidx.wear.phone.interactions.notifications.BridgingManager.Companion Companion;
+ }
+
+ public static final class BridgingManager.Companion {
+ method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+ }
+
+ public final class BridgingManagerService extends android.app.Service {
+ ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+ method public android.os.IBinder? onBind(android.content.Intent? intent);
+ }
+
+}
+
diff --git a/wear/wear-phone-interactions/api/current.txt b/wear/wear-phone-interactions/api/current.txt
index 4659ee8..22d6f21 100644
--- a/wear/wear-phone-interactions/api/current.txt
+++ b/wear/wear-phone-interactions/api/current.txt
@@ -7,6 +7,7 @@
field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+ field public static final int DEVICE_TYPE_NONE = 4; // 0x4
field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
}
@@ -15,6 +16,7 @@
property public static final int DEVICE_TYPE_ANDROID;
property public static final int DEVICE_TYPE_ERROR;
property public static final int DEVICE_TYPE_IOS;
+ property public static final int DEVICE_TYPE_NONE;
property public static final int DEVICE_TYPE_UNKNOWN;
}
diff --git a/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt b/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt
diff --git a/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt b/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..0798d72
--- /dev/null
+++ b/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,173 @@
+// Signature format: 4.0
+package androidx.wear.phone.interactions {
+
+ public final class PhoneTypeHelper {
+ method public static int getPhoneDeviceType(android.content.Context context);
+ field public static final androidx.wear.phone.interactions.PhoneTypeHelper.Companion Companion;
+ field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
+ field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
+ field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+ field public static final int DEVICE_TYPE_NONE = 4; // 0x4
+ field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
+ }
+
+ public static final class PhoneTypeHelper.Companion {
+ method public int getPhoneDeviceType(android.content.Context context);
+ property public static final int DEVICE_TYPE_ANDROID;
+ property public static final int DEVICE_TYPE_ERROR;
+ property public static final int DEVICE_TYPE_IOS;
+ property public static final int DEVICE_TYPE_NONE;
+ property public static final int DEVICE_TYPE_UNKNOWN;
+ }
+
+}
+
+package androidx.wear.phone.interactions.authentication {
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
+ ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
+ method public String getValue();
+ property public final String value;
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
+ ctor public CodeVerifier();
+ ctor public CodeVerifier(optional int byteLength);
+ ctor public CodeVerifier(String value);
+ method public String getValue();
+ property public final String value;
+ }
+
+ public final class OAuthRequest {
+ method public String getPackageName();
+ method public String getRedirectUrl();
+ method public android.net.Uri getRequestUrl();
+ property public final String packageName;
+ property public final String redirectUrl;
+ property public final android.net.Uri requestUrl;
+ field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
+ field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
+ field public static final String WEAR_REDIRECT_URL_PREFIX_CN = "https://wear.googleapis-cn.com/3p_auth/";
+ }
+
+ public static final class OAuthRequest.Builder {
+ ctor public OAuthRequest.Builder(android.content.Context context);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.wear.phone.interactions.authentication.OAuthRequest build();
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setAuthProviderUrl(android.net.Uri authProviderUrl);
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setClientId(String clientId);
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setCodeChallenge(androidx.wear.phone.interactions.authentication.CodeChallenge codeChallenge);
+ method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setRedirectUrl(android.net.Uri redirectUrl);
+ }
+
+ public static final class OAuthRequest.Companion {
+ property public static final String WEAR_REDIRECT_URL_PREFIX;
+ property public static final String WEAR_REDIRECT_URL_PREFIX_CN;
+ }
+
+ public final class OAuthResponse {
+ method public int getErrorCode();
+ method public android.net.Uri? getResponseUrl();
+ property @androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode public final int errorCode;
+ property public final android.net.Uri? responseUrl;
+ }
+
+ public static final class OAuthResponse.Builder {
+ ctor public OAuthResponse.Builder();
+ method public androidx.wear.phone.interactions.authentication.OAuthResponse build();
+ method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setErrorCode(@androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode int errorCode);
+ method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setResponseUrl(android.net.Uri responseUrl);
+ }
+
+ public final class RemoteAuthClient implements java.lang.AutoCloseable {
+ method @UiThread public void close();
+ method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+ method protected void finalize();
+ method public kotlinx.coroutines.flow.Flow<java.lang.Integer> getAvailabilityStatus();
+ method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+ property public final kotlinx.coroutines.flow.Flow<java.lang.Integer> availabilityStatus;
+ field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
+ field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+ field public static final int NO_ERROR = -1; // 0xffffffff
+ field public static final int STATUS_AVAILABLE = 3; // 0x3
+ field public static final int STATUS_TEMPORARILY_UNAVAILABLE = 2; // 0x2
+ field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
+ public abstract static class RemoteAuthClient.Callback {
+ ctor public RemoteAuthClient.Callback();
+ method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, @androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode int errorCode);
+ method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
+ }
+
+ public static final class RemoteAuthClient.Companion {
+ method public androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+ property public static final int ERROR_PHONE_UNAVAILABLE;
+ property public static final int ERROR_UNSUPPORTED;
+ property public static final int NO_ERROR;
+ property public static final int STATUS_AVAILABLE;
+ property public static final int STATUS_TEMPORARILY_UNAVAILABLE;
+ property public static final int STATUS_UNAVAILABLE;
+ property public static final int STATUS_UNKNOWN;
+ }
+
+ @IntDef({androidx.wear.phone.interactions.authentication.RemoteAuthClient.NO_ERROR, androidx.wear.phone.interactions.authentication.RemoteAuthClient.ERROR_UNSUPPORTED, androidx.wear.phone.interactions.authentication.RemoteAuthClient.ERROR_PHONE_UNAVAILABLE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) public static @interface RemoteAuthClient.Companion.ErrorCode {
+ }
+
+ public interface RemoteAuthRequestHandler {
+ method public boolean isAuthSupported();
+ method public void sendAuthRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+ }
+
+ public abstract class RemoteAuthService extends android.app.Service {
+ ctor public RemoteAuthService();
+ method protected final android.os.IBinder onBind(android.content.Intent intent, androidx.wear.phone.interactions.authentication.RemoteAuthRequestHandler remoteAuthRequestHandler);
+ method public static final void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+ method protected boolean verifyPackageName(android.content.Context context, String? requestPackageName);
+ field public static final androidx.wear.phone.interactions.authentication.RemoteAuthService.Companion Companion;
+ }
+
+ public static final class RemoteAuthService.Companion {
+ method public void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+ }
+
+}
+
+package androidx.wear.phone.interactions.notifications {
+
+ public final class BridgingConfig {
+ method public java.util.Set<java.lang.String>? getExcludedTags();
+ method public boolean isBridgingEnabled();
+ property public final java.util.Set<java.lang.String>? excludedTags;
+ property public final boolean isBridgingEnabled;
+ }
+
+ public static final class BridgingConfig.Builder {
+ ctor public BridgingConfig.Builder(android.content.Context context, boolean isBridgingEnabled);
+ method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTag(String tag);
+ method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTags(java.util.Collection<java.lang.String> tags);
+ method public androidx.wear.phone.interactions.notifications.BridgingConfig build();
+ }
+
+ public fun interface BridgingConfigurationHandler {
+ method public void applyBridgingConfiguration(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+ }
+
+ public final class BridgingManager {
+ method public static androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+ method public void setConfig(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+ field public static final androidx.wear.phone.interactions.notifications.BridgingManager.Companion Companion;
+ }
+
+ public static final class BridgingManager.Companion {
+ method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+ }
+
+ public final class BridgingManagerService extends android.app.Service {
+ ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+ method public android.os.IBinder? onBind(android.content.Intent? intent);
+ }
+
+}
+
diff --git a/wear/wear-phone-interactions/api/restricted_current.txt b/wear/wear-phone-interactions/api/restricted_current.txt
index 8b63c78..0798d72 100644
--- a/wear/wear-phone-interactions/api/restricted_current.txt
+++ b/wear/wear-phone-interactions/api/restricted_current.txt
@@ -7,6 +7,7 @@
field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+ field public static final int DEVICE_TYPE_NONE = 4; // 0x4
field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
}
@@ -15,6 +16,7 @@
property public static final int DEVICE_TYPE_ANDROID;
property public static final int DEVICE_TYPE_ERROR;
property public static final int DEVICE_TYPE_IOS;
+ property public static final int DEVICE_TYPE_NONE;
property public static final int DEVICE_TYPE_UNKNOWN;
}
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
index 4be01cd..676a0ca 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
@@ -34,9 +34,14 @@
.path(BLUETOOTH_MODE)
.build()
+ /**
+ * These values follow the values of platform constants defined in
+ * [Settings.Global.Wearable.PAIRED_DEVICE_OS_TYPE].
+ */
internal const val UNKNOWN_MODE = 0
internal const val ANDROID_MODE = 1
internal const val IOS_MODE = 2
+ internal const val NONE_PAIRED_MODE = 3
/** Indicates an error returned retrieving the type of phone we are paired to. */
public const val DEVICE_TYPE_ERROR: Int = 0
@@ -50,18 +55,22 @@
/** Indicates unknown type of phone we are paired to. */
public const val DEVICE_TYPE_UNKNOWN: Int = 3
+ /** Indicates that device is not paired to phone. */
+ public const val DEVICE_TYPE_NONE: Int = 4
+
/**
* Returns the type of phone handset this Wear OS device has been paired with.
*
- * @return one of `DEVICE_TYPE_ERROR`, `DEVICE_TYPE_ANDROID`, `DEVICE_TYPE_IOS` or
- * `DEVICE_TYPE_UNKNOWN` indicating we had an error while determining the phone type, we
- * are paired to an Android phone, we are paired to an iOS phone or we could not determine
- * the phone type respectively.
+ * @return one of `DEVICE_TYPE_ERROR`, `DEVICE_TYPE_ANDROID`, `DEVICE_TYPE_IOS`,
+ * `DEVICE_TYPE_UNKNOWN` or `DEVICE_TYPE_NONE` indicating we had an error while
+ * determining the phone type, we are paired to an Android phone, we are paired to an iOS
+ * phone, we could not determine the phone type respectively, or no phone is paired
+ * respectively.
*/
@DeviceFamily
@JvmStatic
public fun getPhoneDeviceType(context: Context): Int {
- var bluetoothMode = UNKNOWN_MODE
+ var pairedDeviceType = UNKNOWN_MODE
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
val cursor =
context.contentResolver.query(BLUETOOTH_MODE_URI, null, null, null, null)
@@ -69,34 +78,41 @@
cursor.use {
while (it.moveToNext()) {
if (BLUETOOTH_MODE == it.getString(0)) {
- bluetoothMode = it.getInt(1)
+ pairedDeviceType = it.getInt(1)
break
}
}
}
+ return when (pairedDeviceType) {
+ ANDROID_MODE -> DEVICE_TYPE_ANDROID
+ IOS_MODE -> DEVICE_TYPE_IOS
+ else -> DEVICE_TYPE_UNKNOWN
+ }
} else if (
Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
context.applicationInfo.targetSdkVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE
) {
return DEVICE_TYPE_ANDROID
- } else {
- bluetoothMode =
- Settings.Global.getInt(
- context.contentResolver,
- PAIRED_DEVICE_OS_TYPE,
- UNKNOWN_MODE
- )
}
- return when (bluetoothMode) {
+ pairedDeviceType =
+ Settings.Global.getInt(context.contentResolver, PAIRED_DEVICE_OS_TYPE, UNKNOWN_MODE)
+ return when (pairedDeviceType) {
ANDROID_MODE -> DEVICE_TYPE_ANDROID
IOS_MODE -> DEVICE_TYPE_IOS
+ NONE_PAIRED_MODE -> DEVICE_TYPE_NONE
else -> DEVICE_TYPE_UNKNOWN
}
}
/** Annotates a value of DeviceType. */
@Retention(AnnotationRetention.SOURCE)
- @IntDef(DEVICE_TYPE_ERROR, DEVICE_TYPE_ANDROID, DEVICE_TYPE_IOS, DEVICE_TYPE_UNKNOWN)
+ @IntDef(
+ DEVICE_TYPE_ERROR,
+ DEVICE_TYPE_ANDROID,
+ DEVICE_TYPE_IOS,
+ DEVICE_TYPE_UNKNOWN,
+ DEVICE_TYPE_NONE
+ )
internal annotation class DeviceFamily
}
}
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
index afcf1bb..57cc0e9 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
@@ -175,6 +175,18 @@
.isEqualTo(PhoneTypeHelper.DEVICE_TYPE_IOS)
}
+ @Test
+ @Config(minSdk = 35)
+ fun testGetDeviceType_returnsNone() {
+ Settings.Global.putInt(
+ contentResolver,
+ PhoneTypeHelper.PAIRED_DEVICE_OS_TYPE,
+ PhoneTypeHelper.NONE_PAIRED_MODE
+ )
+ assertThat(getPhoneDeviceType(ApplicationProvider.getApplicationContext()))
+ .isEqualTo(PhoneTypeHelper.DEVICE_TYPE_NONE)
+ }
+
companion object {
private fun createFakeBluetoothModeCursor(bluetoothMode: Int): Cursor {
val cursor = MatrixCursor(arrayOf("key", "value"))
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 23ee69f..4a0366f 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -422,8 +422,8 @@
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.PrerenderOperationCallback);
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.PrerenderOperationCallback);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void removeWebMessageListener(android.webkit.WebView, String);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.MUTE_AUDIO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void setAudioMuted(android.webkit.WebView, boolean);
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.DEFAULT_TRAFFICSTATS_TAGGING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDefaultTrafficStatsTag(int);
@@ -478,7 +478,7 @@
field public static final String MUTE_AUDIO = "MUTE_AUDIO";
field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+ field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 23ee69f..4a0366f 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -422,8 +422,8 @@
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.PrerenderOperationCallback);
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.PrerenderOperationCallback);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void removeWebMessageListener(android.webkit.WebView, String);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.MUTE_AUDIO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void setAudioMuted(android.webkit.WebView, boolean);
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.DEFAULT_TRAFFICSTATS_TAGGING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDefaultTrafficStatsTag(int);
@@ -478,7 +478,7 @@
field public static final String MUTE_AUDIO = "MUTE_AUDIO";
field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+ field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 0fde2c0..a461177 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -22,6 +22,7 @@
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.webkit.ValueCallback;
@@ -35,7 +36,6 @@
import androidx.annotation.RequiresOptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
-import androidx.core.os.CancellationSignal;
import androidx.webkit.internal.ApiFeature;
import androidx.webkit.internal.ApiHelperForM;
import androidx.webkit.internal.ApiHelperForO;
@@ -1341,8 +1341,7 @@
}
/**
- * Starts a URL prerender request for this WebView. Can be called from any
- * thread.
+ * Starts a URL prerender request for this WebView. Must be called from the UI thread.
* <p>
* This WebView will use a URL request matching algorithm during execution
* of all variants of {@link android.webkit.WebView#loadUrl(String)} for
@@ -1363,26 +1362,26 @@
* <p>
* The {@link CancellationSignal} will make the best effort to cancel an
* in-flight prerender request; however cancellation it is not guaranteed.
- * <p>
- * All result callbacks will be resolved on the calling thread.
*
* @param webView the WebView for which we trigger the prerender request.
* @param url the url associated with the prerender request.
* @param cancellationSignal used to trigger prerender cancellation.
+ * @param callbackExecutor the executor to resolve the callback with.
* @param callback callbacks for reporting result back to application.
*/
@RequiresFeature(name = WebViewFeature.PRERENDER_WITH_URL,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
- @AnyThread
+ @UiThread
@ExperimentalUrlPrerender
- public static void prerenderUrlAsync(
+ public static void prerenderUrl(
@NonNull WebView webView,
@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull PrerenderOperationCallback callback) {
ApiFeature.NoFramework feature = WebViewFeatureInternal.PRERENDER_WITH_URL;
if (feature.isSupportedByWebView()) {
- getProvider(webView).prerenderUrlAsync(url, cancellationSignal, callback);
+ getProvider(webView).prerenderUrl(url, cancellationSignal, callbackExecutor, callback);
} else {
throw WebViewFeatureInternal.getUnsupportedOperationException();
}
@@ -1390,28 +1389,31 @@
/**
* The same as
- * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal, PrerenderOperationCallback)},
+ * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor, PrerenderOperationCallback)},
* but allows customizing the request by providing {@link SpeculativeLoadingParameters}.
*
* @param webView the WebView for which we trigger the prerender request.
* @param url the url associated with the prerender request.
* @param cancellationSignal used to trigger prerender cancellation.
+ * @param callbackExecutor the executor to resolve the callback with.
* @param params parameters to customize the prerender request.
* @param callback callbacks for reporting result back to application.
*/
@RequiresFeature(name = WebViewFeature.PRERENDER_WITH_URL,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
- @AnyThread
+ @UiThread
@ExperimentalUrlPrerender
- public static void prerenderUrlAsync(
+ public static void prerenderUrl(
@NonNull WebView webView,
@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull SpeculativeLoadingParameters params,
@NonNull PrerenderOperationCallback callback) {
ApiFeature.NoFramework feature = WebViewFeatureInternal.PRERENDER_WITH_URL;
if (feature.isSupportedByWebView()) {
- getProvider(webView).prerenderUrlAsync(url, cancellationSignal, params, callback);
+ getProvider(webView).prerenderUrl(url, cancellationSignal, callbackExecutor, params,
+ callback);
} else {
throw WebViewFeatureInternal.getUnsupportedOperationException();
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 18747e3..2411346 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -653,11 +653,11 @@
/**
* Feature for {@link #isFeatureSupported(String)}.
* This feature covers
- * {@link androidx.webkit.WebViewCompat#prerenderUrlAsync(WebView, String,
- * SpeculativeLoadingParameters, CancellationSignal, PrerenderOperationCallback)}}
+ * {@link androidx.webkit.WebViewCompat#prerenderUrl(WebView, String, CancellationSignal,
+ * Executor, SpeculativeLoadingParameters, PrerenderOperationCallback)}}
*/
@WebViewCompat.ExperimentalUrlPrerender
- public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+ public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
/**
* Feature for {@link #isFeatureSupported(String)}.
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index fd040be..df7473b 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -31,6 +31,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.webkit.OutcomeReceiverCompat;
+import androidx.webkit.PrerenderOperationCallback;
import androidx.webkit.Profile;
import androidx.webkit.ProfileStore;
import androidx.webkit.ProxyConfig;
@@ -689,8 +690,9 @@
/**
* Feature for {@link WebViewFeature#isFeatureSupported(String)}.
- * This feature covers {@link androidx.webkit.WebViewCompat#prerenderUrlAsync(WebView, String,
- * SpeculativeLoadingParameters, CancellationSignal, PrerenderOperationCallback)}}
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
+ * SpeculativeLoadingParameters, PrerenderOperationCallback)}}
*/
public static final ApiFeature.NoFramework PRERENDER_WITH_URL =
new ApiFeature.NoFramework(WebViewFeature.PRERENDER_WITH_URL,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 0bfb95e..44b00b1 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -18,11 +18,13 @@
import android.annotation.SuppressLint;
import android.net.Uri;
+import android.os.CancellationSignal;
+import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
-import androidx.core.os.CancellationSignal;
+import androidx.webkit.PrerenderException;
import androidx.webkit.PrerenderOperationCallback;
import androidx.webkit.Profile;
import androidx.webkit.SpeculativeLoadingParameters;
@@ -194,24 +196,58 @@
/**
* Adapter method for
- * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal,
+ * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
* PrerenderOperationCallback)}.
*/
- public void prerenderUrlAsync(
+ public void prerenderUrl(
@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull PrerenderOperationCallback callback) {
+
+ ValueCallback<Void> activationCallback = (value) -> {
+ // value will always be null.
+ callback.onPrerenderActivated();
+ };
+ ValueCallback<Throwable> errorCallback = (throwable) -> {
+ callback.onError(new PrerenderException("Prerender operation failed", throwable));
+ };
+ mImpl.prerenderUrl(
+ url,
+ cancellationSignal,
+ callbackExecutor,
+ activationCallback,
+ errorCallback);
}
/**
* Adapter method for
- * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal,
+ * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
* SpeculativeLoadingParameters, PrerenderOperationCallback)}.
*/
- public void prerenderUrlAsync(
+ public void prerenderUrl(
@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull SpeculativeLoadingParameters params,
@NonNull PrerenderOperationCallback callback) {
+
+ InvocationHandler paramsBoundaryInterface =
+ BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+ new SpeculativeLoadingParametersAdapter(params));
+ ValueCallback<Void> activationCallback = (value) -> {
+ // value will always be null.
+ callback.onPrerenderActivated();
+ };
+ ValueCallback<Throwable> errorCallback = (throwable) -> {
+ callback.onError(new PrerenderException("Prerender operation failed", throwable));
+ };
+ mImpl.prerenderUrl(
+ url,
+ cancellationSignal,
+ callbackExecutor,
+ paramsBoundaryInterface,
+ activationCallback,
+ errorCallback);
}
}