Merge "Fix: Crash IllegalArgumentException due to difference in mMaxPages and numPages" into androidx-main
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 11684ed..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,31 +17,21 @@
package androidx.appfunctions
import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_APP
-import androidx.core.util.Preconditions
/**
* 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.
- *
- * <p>Reports errors of the category [ERROR_CATEGORY_APP].
*/
public abstract class AppFunctionAppException
internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(errorCode, errorMessage, extras) {
- init {
- Preconditions.checkArgument(errorCategory == ERROR_CATEGORY_APP)
- }
-}
+ 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) :
@@ -59,8 +49,6 @@
*
* <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) :
@@ -74,8 +62,6 @@
*
* <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) :
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 1813e6c..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,32 +17,22 @@
package androidx.appfunctions
import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_REQUEST_ERROR
-import androidx.core.util.Preconditions
/**
* 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.
- *
- * <p>Reports errors of the category [ERROR_CATEGORY_REQUEST_ERROR].
*/
public abstract class AppFunctionRequestException
internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(errorCode, errorMessage, extras) {
- init {
- Preconditions.checkArgument(errorCategory == ERROR_CATEGORY_REQUEST_ERROR)
- }
-}
+ 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) :
@@ -55,8 +45,6 @@
* 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
@@ -70,8 +58,6 @@
* 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
@@ -81,11 +67,7 @@
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) :
AppFunctionRequestException(ERROR_FUNCTION_NOT_FOUND, errorMessage, extras) {
@@ -93,11 +75,7 @@
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) :
AppFunctionRequestException(ERROR_RESOURCE_NOT_FOUND, errorMessage, extras) {
@@ -105,11 +83,7 @@
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) :
AppFunctionRequestException(ERROR_LIMIT_EXCEEDED, errorMessage, extras) {
@@ -120,8 +94,6 @@
/**
* 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) :
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 5f38a8d..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,20 @@
package androidx.appfunctions
import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_SYSTEM
-import androidx.core.util.Preconditions
/**
* Thrown when an internal unexpected error comes from the system.
*
* <p>For example, the AppFunctionService implementation is not found by the system.
- *
- * <p>Reports errors of the category [ERROR_CATEGORY_SYSTEM].
*/
public abstract class AppFunctionSystemException
internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
- AppFunctionException(errorCode, errorMessage, extras) {
- init {
- Preconditions.checkArgument(errorCategory == ERROR_CATEGORY_SYSTEM)
- }
-}
+ 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.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
*/
public class AppFunctionSystemUnknownException
internal constructor(errorMessage: String? = null, extras: Bundle) :
@@ -49,11 +39,7 @@
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) :
AppFunctionSystemException(ERROR_CANCELLED, errorMessage, extras) {
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/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/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/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/libraryversions.toml b/libraryversions.toml
index 4158de0..59c4949 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -168,7 +168,7 @@
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_PHONE_INTERACTIONS = "1.1.0-beta01"
WEAR_PROTOLAYOUT = "1.3.0-alpha07"
WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
WEAR_TILES = "1.5.0-alpha07"
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 222e6bd..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
@@ -299,7 +299,15 @@
} else {
switchContentFragment(ResizeFragment(), menuItem.title)
}
- R.id.item_scroll -> switchContentFragment(ScrollFragment(), 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/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/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-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/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/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
index e28ae0c..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,10 +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 = 4
+ 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
@@ -66,7 +70,7 @@
@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)
@@ -74,25 +78,25 @@
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