Merge "Cancel graph state collection after UseCaseCamera is closed" into androidx-main
diff --git a/OWNERS b/OWNERS
index b6e6906..12e1923 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,3 +28,6 @@
per-file *settings.gradle = set noparent
per-file *settings.gradle = nickanthony@google.com, alanv@google.com, aurimas@google.com
per-file *libraryversions.toml = jnichol@google.com
+
+# Copybara can self-approve CLs within synced docs.
+per-file docs/... = copybara-worker-blackhole@google.com
\ No newline at end of file
diff --git a/arch/core/core-common/api/restricted_current.txt b/arch/core/core-common/api/restricted_current.txt
index 7f1c354..4fbc435 100644
--- a/arch/core/core-common/api/restricted_current.txt
+++ b/arch/core/core-common/api/restricted_current.txt
@@ -20,11 +20,15 @@
method public int size();
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions implements java.util.Iterator<java.util.Map.Entry<K,V>> {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K,V> implements java.util.Iterator<java.util.Map.Entry<K,V>> {
method public boolean hasNext();
method public java.util.Map.Entry<K!,V!>! next();
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove<K, V> {
+ ctor public SafeIterableMap.SupportRemove();
+ }
+
}
package androidx.arch.core.util {
diff --git a/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java b/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
index 08cce2f..bdaeca8 100644
--- a/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
+++ b/arch/core/core-common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
@@ -358,7 +358,14 @@
}
}
- abstract static class SupportRemove<K, V> {
+ /**
+ * @hide
+ *
+ * @param <K>
+ * @param <V>
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public abstract static class SupportRemove<K, V> {
abstract void supportRemove(@NonNull Entry<K, V> entry);
}
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index 680dd62..0f0b95f 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -476,6 +476,15 @@
fun pathExists(absoluteFilePath: String): Boolean {
return ShellImpl.executeCommandUnsafe("ls $absoluteFilePath").trim() == absoluteFilePath
}
+
+ @RequiresApi(21)
+ fun amBroadcast(broadcastArguments: String): Int? {
+ // unsafe here for perf, since we validate the return value so we don't need to check stderr
+ return ShellImpl.executeCommandUnsafe("am broadcast $broadcastArguments")
+ .substringAfter("Broadcast completed: result=")
+ .trim()
+ .toIntOrNull()
+ }
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index aeec388..685451c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -270,4 +270,13 @@
fun dropShaderCachePublicApi() = validateDropShaderCacheWithRoot {
dropShaderCache()
}
+
+ @Test
+ fun dropKernelPageCache() {
+ val scope = MacrobenchmarkScope(
+ Packages.TARGET,
+ launchWithClearTask = false
+ )
+ scope.dropKernelPageCache() // shouldn't crash
+ }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
index 0da5134..055ccfd 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -17,16 +17,23 @@
package androidx.benchmark.macro
import android.os.Build
+import androidx.benchmark.junit4.PerfettoTraceRule
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import kotlin.test.assertNull
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class ProfileInstallBroadcastTest {
+ @OptIn(ExperimentalPerfettoCaptureApi::class)
+ @get:Rule
+ val perfettoTraceRule = PerfettoTraceRule()
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
@Test
fun installProfile() {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index ff6b62a..088d93a 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -22,6 +22,7 @@
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOWEST_BUNDLED_VERSION_SUPPORTED
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.testutils.verifyWithPolling
@@ -60,10 +61,12 @@
PerfettoHelper.stopAllPerfettoProcesses()
}
+ @FlakyTest(bugId = 258216025)
@SdkSuppress(minSdkVersion = LOWEST_BUNDLED_VERSION_SUPPORTED)
@Test
fun captureAndValidateTrace_bundled() = captureAndValidateTrace(unbundled = false)
+ @FlakyTest(bugId = 258216025)
@Test
fun captureAndValidateTrace_unbundled() = captureAndValidateTrace(unbundled = true)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 296d682..0d3280b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -294,11 +294,11 @@
if (Build.VERSION.SDK_INT >= 31) {
dropKernelPageCacheSetProp()
} else {
- val result = Shell.executeScriptCaptureStdout(
+ val result = Shell.executeScriptCaptureStdoutStderr(
"echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure"
- ).trim()
+ )
// Older user builds don't allow drop caches, should investigate workaround
- if (result != "Success") {
+ if (result.stdout.trim() != "Success") {
if (DeviceInfo.isRooted && !Shell.isSessionRooted()) {
throw IllegalStateException("Failed to drop caches - run `adb root`")
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index dc8d53f..f81127b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -16,6 +16,7 @@
package androidx.benchmark.macro
+import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.benchmark.Shell
@@ -26,7 +27,8 @@
private val receiverName = ProfileInstallReceiver::class.java.name
/**
- * Returns null on success, or an error string otherwise.
+ * Returns null on success, error string on suppress-able error, or throws if profileinstaller
+ * not up to date.
*
* Returned error strings aren't thrown, to let the calling function decide strictness.
*/
@@ -36,13 +38,7 @@
// installed synchronously
val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
// Use an explicit broadcast given the app was force-stopped.
- val result = Shell.executeScriptCaptureStdout(
- "am broadcast -a $action $packageName/$receiverName"
- )
- .substringAfter("Broadcast completed: result=")
- .trim()
- .toIntOrNull()
- when (result) {
+ when (val result = Shell.amBroadcast("-a $action $packageName/$receiverName")) {
null,
// 0 is returned by the platform by default, and also if no broadcast receiver
// receives the broadcast.
@@ -63,12 +59,25 @@
)
}
ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
+ val sdkInt = Build.VERSION.SDK_INT
throw RuntimeException(
- "Baseline profiles aren't supported on this device version"
+ if (sdkInt <= 23) {
+ "Baseline profiles aren't supported on this device version," +
+ " as all apps are fully ahead-of-time compiled."
+ } else {
+ "The device SDK version ($sdkInt) isn't supported" +
+ " by the target app's copy of profileinstaller." +
+ if (sdkInt in 31..33) {
+ " Please use profileinstaller `1.2.1`" +
+ " or newer for API 31-33 support"
+ } else {
+ ""
+ }
+ }
)
}
ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
- return "No baseline profile was found in the target apk."
+ return "No baseline profile was found in the target apk."
}
ProfileInstaller.RESULT_NOT_WRITABLE,
ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
@@ -103,12 +112,7 @@
val action = "androidx.profileinstaller.action.SKIP_FILE"
val operationKey = "EXTRA_SKIP_FILE_OPERATION"
val extras = "$operationKey $operation"
- val result = Shell.executeScriptCaptureStdout(
- "am broadcast -a $action -e $extras $packageName/$receiverName"
- )
- .substringAfter("Broadcast completed: result=")
- .trim()
- .toIntOrNull()
+ val result = Shell.amBroadcast("-a $action -e $extras $packageName/$receiverName")
return when {
result == null || result == 0 -> {
// 0 is returned by the platform by default, and also if no broadcast receiver
@@ -143,13 +147,7 @@
fun saveProfile(packageName: String): String? {
Log.d(TAG, "Profile Installer - Save Profile")
val action = "androidx.profileinstaller.action.SAVE_PROFILE"
- val result = Shell.executeScriptCaptureStdout(
- "am broadcast -a $action $packageName/$receiverName"
- )
- .substringAfter("Broadcast completed: result=")
- .trim()
- .toIntOrNull()
- return when (result) {
+ return when (val result = Shell.amBroadcast("-a $action $packageName/$receiverName")) {
null, 0 -> {
// 0 is returned by the platform by default, and also if no broadcast receiver
// receives the broadcast.
@@ -176,20 +174,19 @@
}
}
- private fun benchmarkOperation(packageName: String, operation: String): String? {
+ private fun benchmarkOperation(
+ packageName: String,
+ @Suppress("SameParameterValue") operation: String
+ ): String? {
Log.d(TAG, "Profile Installer - Benchmark Operation: $operation")
// Redefining constants here, because these are only defined in the latest alpha for
// ProfileInstaller.
// Use an explicit broadcast given the app was force-stopped.
val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
val operationKey = "EXTRA_BENCHMARK_OPERATION"
- val extras = "$operationKey $operation"
- val result = Shell.executeScriptCaptureStdout(
- "am broadcast -a $action -e $extras $packageName/$receiverName"
+ val result = Shell.amBroadcast(
+ "-a $action -e $operationKey $operation $packageName/$receiverName"
)
- .substringAfter("Broadcast completed: result=")
- .trim()
- .toIntOrNull()
return when (result) {
null, 0, 16 /* BENCHMARK_OPERATION_UNKNOWN */ -> {
// 0 is returned by the platform by default, and also if no broadcast receiver
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt
index 8b88add..5095f08 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertiseCallback.kt
@@ -18,6 +18,7 @@
import android.bluetooth.le.AdvertiseSettings as FwkAdvertiseSettings
+@JvmDefaultWithCompatibility
/**
* Bluetooth LE advertising callbacks, used to deliver advertising operation status.
*
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt
index 4b8f628..4f77e63 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/AdvertisingSetCallback.kt
@@ -19,6 +19,7 @@
import android.bluetooth.le.AdvertisingSetCallback as FwkAdvertisingSetCallback
import android.bluetooth.le.AdvertisingSet as FwkAdvertisingSet
+@JvmDefaultWithCompatibility
/**
* Bluetooth LE advertising set callbacks, used to deliver advertising operation
* status.
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt
index 6b4e653..cc28712 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothDevice.kt
@@ -957,6 +957,7 @@
return impl.toBundle()
}
+ @JvmDefaultWithCompatibility
interface BluetoothDeviceImpl : Bundleable {
fun connectGatt(
context: Context,
diff --git a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt
index adba929..2133332 100644
--- a/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt
+++ b/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/ScanCallback.kt
@@ -21,6 +21,7 @@
import androidx.annotation.IntDef
+@JvmDefaultWithCompatibility
/**
* Bluetooth LE scan callbacks. Scan results are reported using these callbacks.
*
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 91d7c75..98ad678 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -298,9 +298,14 @@
} else {
task.kotlinOptions.jvmTarget = "1.8"
}
- task.kotlinOptions.freeCompilerArgs += listOf(
- "-Xskip-metadata-version-check"
+ val kotlinCompilerArgs = mutableListOf(
+ "-Xskip-metadata-version-check",
)
+ // TODO (b/259578592): enable -Xjvm-default=all for camera-camera2-pipe projects
+ if (!project.name.contains("camera-camera2-pipe")) {
+ kotlinCompilerArgs += "-Xjvm-default=all"
+ }
+ task.kotlinOptions.freeCompilerArgs += kotlinCompilerArgs
}
// If no one else is going to register a source jar, then we should.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 7021369..0e99e30 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -16,6 +16,8 @@
package androidx.camera.camera2.pipe.integration.impl
+import android.media.MediaCodec
+import android.os.Build
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.core.Log
@@ -26,8 +28,9 @@
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
-import androidx.camera.core.ImageCapture
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
@@ -259,10 +262,14 @@
@GuardedBy("lock")
private fun shouldAddRepeatingUseCase(runningUseCases: Set<UseCase>): Boolean {
- return !attachedUseCases.contains(meteringRepeating) &&
- runningUseCases.only { it is ImageCapture }
- }
+ val meteringRepeatingEnabled = attachedUseCases.contains(meteringRepeating)
+ val coreLibraryUseCases = runningUseCases.filterNot { it is MeteringRepeating }
+ val onlyVideoCapture = coreLibraryUseCases.onlyVideoCapture()
+ val requireMeteringRepeating = coreLibraryUseCases.requireMeteringRepeating()
+
+ return !meteringRepeatingEnabled && (onlyVideoCapture || requireMeteringRepeating)
+ }
@GuardedBy("lock")
private fun addRepeatingUseCase() {
meteringRepeating.setupSession()
@@ -272,11 +279,13 @@
@GuardedBy("lock")
private fun shouldRemoveRepeatingUseCase(runningUseCases: Set<UseCase>): Boolean {
- val onlyMeteringRepeatingEnabled = runningUseCases.only { it is MeteringRepeating }
- val meteringRepeatingAndNonImageCaptureEnabled =
- runningUseCases.any { it is MeteringRepeating } &&
- runningUseCases.any { it !is MeteringRepeating && it !is ImageCapture }
- return onlyMeteringRepeatingEnabled || meteringRepeatingAndNonImageCaptureEnabled
+ val meteringRepeatingEnabled = runningUseCases.contains(meteringRepeating)
+
+ val coreLibraryUseCases = runningUseCases.filterNot { it is MeteringRepeating }
+ val onlyVideoCapture = coreLibraryUseCases.onlyVideoCapture()
+ val requireMeteringRepeating = coreLibraryUseCases.requireMeteringRepeating()
+
+ return meteringRepeatingEnabled && !onlyVideoCapture && !requireMeteringRepeating
}
@GuardedBy("lock")
@@ -286,11 +295,31 @@
meteringRepeating.onDetached()
}
- /**
- * Returns true when the collection only has elements (1 or more) that verify the predicate,
- * false otherwise.
- */
- private fun <T> Collection<T>.only(predicate: (T) -> Boolean): Boolean {
- return isNotEmpty() && all(predicate)
+ private fun Collection<UseCase>.onlyVideoCapture(): Boolean {
+ return isNotEmpty() && checkSurfaces { _, sessionSurfaces ->
+ sessionSurfaces.isNotEmpty() && sessionSurfaces.all {
+ it.containerClass == MediaCodec::class.java
+ }
+ }
+ }
+
+ private fun Collection<UseCase>.requireMeteringRepeating(): Boolean {
+ return isNotEmpty() && checkSurfaces { repeatingSurfaces, sessionSurfaces ->
+ // There is no repeating UseCases
+ sessionSurfaces.isNotEmpty() && repeatingSurfaces.isEmpty()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ private fun Collection<UseCase>.checkSurfaces(
+ predicate: (
+ repeatingSurfaces: List<DeferrableSurface>,
+ sessionSurfaces: List<DeferrableSurface>
+ ) -> Boolean
+ ): Boolean = ValidatingBuilder().let { validatingBuilder ->
+ forEach { useCase -> validatingBuilder.add(useCase.sessionConfig) }
+ val sessionConfig = validatingBuilder.build()
+ val captureConfig = sessionConfig.repeatingCaptureConfig
+ return predicate(captureConfig.surfaces, sessionConfig.surfaces)
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 2249cca..da13be6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -714,39 +714,6 @@
}
@Test
- fun legacyVideo_suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- }
- }
-
- @Test
fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
val supportedSurfaceCombination = SupportedSurfaceCombination(
@@ -778,46 +745,6 @@
}
@Test
- fun legacyVideo_suggestedResForCustomizeResolutionsNotSupportedInLegacyDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
-
- // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
- val videoResolutionsPairs = listOf(
- Pair.create(ImageFormat.PRIVATE, arrayOf(recordSize))
- )
- val previewResolutionsPairs = listOf(
- Pair.create(ImageFormat.PRIVATE, arrayOf(previewSize))
- )
- // Override the default max resolution in VideoCapture
- val videoCapture =
- androidx.camera.core.VideoCapture.Builder()
- .setMaxResolution(recordSize)
- .setSupportedResolutions(videoResolutionsPairs)
- .build()
- val preview = Preview.Builder()
- .setSupportedResolutions(previewResolutionsPairs)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- }
- }
-
- @Test
fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
val supportedSurfaceCombination = SupportedSurfaceCombination(
@@ -850,52 +777,6 @@
}
}
- @Test
- fun legacyVideo_getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- recordSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- legacyVideoMaximumVideoSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[preview],
- previewSize
- )
- }
-
// (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
@Test
fun suggestedResolutionsForMixedUseCaseInLimitedDevice() {
@@ -1129,60 +1010,6 @@
}
@Test
- fun legacyVideo_getSuggestedResolutionsForCustomizedSupportedResolutions() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, mockCameraMetadata, cameraId,
- mockCamcorderProfileAdapter
- )
- val formatResolutionsPairList: MutableList<Pair<Int, Array<Size>>> = ArrayList()
- formatResolutionsPairList.add(Pair.create(ImageFormat.JPEG, arrayOf(vgaSize)))
- formatResolutionsPairList.add(
- Pair.create(ImageFormat.YUV_420_888, arrayOf(vgaSize))
- )
- formatResolutionsPairList.add(Pair.create(ImageFormat.PRIVATE, arrayOf(vgaSize)))
-
- // Sets use cases customized supported resolutions to 640x480 only.
- val imageCapture = ImageCapture.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val preview = Preview.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedResolutionMap: Map<UseCaseConfig<*>, Size> =
- supportedSurfaceCombination.getSuggestedResolutions(
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // Checks all suggested resolutions will become 640x480.
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- vgaSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- vgaSize
- )
- Truth.assertThat(suggestedResolutionMap).containsEntry(
- useCaseToConfigMap[preview],
- vgaSize
- )
- }
-
- @Test
fun suggestedResolutionsForCustomizedSupportedResolutions() {
// Checks all suggested resolutions will become 640x480.
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index a98af2c..5f4d5d2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.integration.impl
import android.os.Build
+import android.util.Size
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
@@ -28,12 +29,18 @@
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraComponentBuilder
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeCamera
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.runBlocking
+import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@@ -41,6 +48,8 @@
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class UseCaseManagerTest {
+ private val useCaseManagerList = mutableListOf<UseCaseManager>()
+ private val useCaseList = mutableListOf<UseCase>()
private val useCaseThreads by lazy {
val dispatcher = Dispatchers.Default
val cameraScope = CoroutineScope(
@@ -55,11 +64,17 @@
)
}
+ @After
+ fun tearDown() = runBlocking {
+ useCaseManagerList.forEach { it.close() }
+ useCaseList.forEach { it.onDetached() }
+ }
+
@Test
fun enabledUseCasesEmpty_whenUseCaseAttachedOnly() {
// Arrange
val useCaseManager = createUseCaseManager()
- val useCase = Preview.Builder().build()
+ val useCase = createPreview()
// Act
useCaseManager.attach(listOf(useCase))
@@ -73,7 +88,7 @@
fun enabledUseCasesNotEmpty_whenUseCaseEnabled() {
// Arrange
val useCaseManager = createUseCaseManager()
- val useCase = Preview.Builder().build()
+ val useCase = createPreview()
useCaseManager.attach(listOf(useCase))
// Act
@@ -88,8 +103,8 @@
fun meteringRepeatingNotEnabled_whenPreviewEnabled() {
// Arrange
val useCaseManager = createUseCaseManager()
- val preview = Preview.Builder().build()
- val imageCapture = ImageCapture.Builder().build()
+ val preview = createPreview()
+ val imageCapture = createImageCapture()
useCaseManager.attach(listOf(preview, imageCapture))
// Act
@@ -105,7 +120,7 @@
fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() {
// Arrange
val useCaseManager = createUseCaseManager()
- val imageCapture = ImageCapture.Builder().build()
+ val imageCapture = createImageCapture()
useCaseManager.attach(listOf(imageCapture))
// Act
@@ -123,12 +138,12 @@
fun meteringRepeatingDisabled_whenPreviewBecomesEnabled() {
// Arrange
val useCaseManager = createUseCaseManager()
- val imageCapture = ImageCapture.Builder().build()
+ val imageCapture = createImageCapture()
useCaseManager.attach(listOf(imageCapture))
useCaseManager.activate(imageCapture)
// Act
- val preview = Preview.Builder().build()
+ val preview = createPreview()
useCaseManager.attach(listOf(preview))
useCaseManager.activate(preview)
@@ -141,8 +156,8 @@
fun meteringRepeatingEnabled_afterAllUseCasesButImageCaptureDisabled() {
// Arrange
val useCaseManager = createUseCaseManager()
- val preview = Preview.Builder().build()
- val imageCapture = ImageCapture.Builder().build()
+ val preview = createPreview()
+ val imageCapture = createImageCapture()
useCaseManager.attach(listOf(preview, imageCapture))
useCaseManager.activate(preview)
useCaseManager.activate(imageCapture)
@@ -162,7 +177,7 @@
fun meteringRepeatingDisabled_whenAllUseCasesDisabled() {
// Arrange
val useCaseManager = createUseCaseManager()
- val imageCapture = ImageCapture.Builder().build()
+ val imageCapture = createImageCapture()
useCaseManager.attach(listOf(imageCapture))
useCaseManager.activate(imageCapture)
@@ -187,6 +202,36 @@
ComboRequestListener()
),
cameraStateAdapter = CameraStateAdapter(),
- displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext()),
- )
+ displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+ ).also {
+ useCaseManagerList.add(it)
+ }
+
+ private fun createImageCapture(): ImageCapture =
+ ImageCapture.Builder()
+ .setCaptureOptionUnpacker { _, _ -> }
+ .setSessionOptionUnpacker() { _, _ -> }
+ .build().also {
+ it.simulateActivation()
+ useCaseList.add(it)
+ }
+
+ private fun createPreview(): Preview =
+ Preview.Builder()
+ .setCaptureOptionUnpacker { _, _ -> }
+ .setSessionOptionUnpacker() { _, _ -> }
+ .build().apply {
+ setSurfaceProvider(
+ CameraXExecutors.mainThreadExecutor(),
+ SurfaceTextureProvider.createSurfaceTextureProvider()
+ )
+ }.also {
+ it.simulateActivation()
+ useCaseList.add(it)
+ }
+
+ private fun UseCase.simulateActivation() {
+ onAttach(FakeCamera("0"), null, null)
+ updateSuggestedResolution(Size(640, 480))
+ }
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
deleted file mode 100644
index b8b5535..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.ContentValues
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.media.MediaRecorder
-import android.os.Build
-import android.os.ParcelFileDescriptor
-import android.provider.MediaStore
-import android.util.Size
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Logger
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.AudioUtil
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
-import androidx.camera.testing.SurfaceTextureProvider.createSurfaceTextureProvider
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import androidx.testutils.assertThrows
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@Suppress("DEPRECATION")
-@SdkSuppress(minSdkVersion = 21)
-class VideoCaptureTest {
- companion object {
- private const val TAG = "VideoCaptureTest"
- }
-
- @get:Rule
- val useRecordingResource = CameraUtil.checkVideoRecordingResource()
-
- @get:Rule
- val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
- )
-
- @get:Rule
- val permissionRule: GrantPermissionRule =
- GrantPermissionRule.grant(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.RECORD_AUDIO
- )
-
- private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
- private val context = ApplicationProvider.getApplicationContext<Context>()
-
- private lateinit var cameraSelector: CameraSelector
-
- private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-
- private lateinit var contentResolver: ContentResolver
-
- @Before
- fun setUp() {
- // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
- assumeFalse(
- "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
- Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
- )
-
- assumeTrue(CameraUtil.deviceHasCamera())
- assumeTrue(AudioUtil.canStartAudioRecord(MediaRecorder.AudioSource.CAMCORDER))
-
- cameraSelector = if (CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
- CameraSelector.DEFAULT_BACK_CAMERA
- } else {
- CameraSelector.DEFAULT_FRONT_CAMERA
- }
-
- CameraXUtil.initialize(
- context,
- Camera2Config.defaultConfig()
- ).get()
- cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-
- contentResolver = context.contentResolver
- }
-
- @After
- fun tearDown() {
- instrumentation.runOnMainSync {
- if (this::cameraUseCaseAdapter.isInitialized) {
- cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
- }
- }
-
- CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 25)
- fun buildFileOutputOptionsWithFileDescriptor_throwExceptionWhenAPILevelSmallerThan26() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- val fileDescriptor =
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE).fileDescriptor
-
- assertThrows<IllegalArgumentException> {
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(fileDescriptor).build()
- }
-
- file.delete()
- }
-
- @Test(timeout = 30000)
- @SdkSuppress(minSdkVersion = 26)
- fun startRecordingWithFileDescriptor_whenAPILevelLargerThan26() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- // It's needed to have a variable here to hold the parcel file descriptor reference which
- // returned from ParcelFileDescriptor.open(), the returned parcel descriptor reference might
- // be garbage collected unexpectedly. That will caused an "invalid file descriptor" issue.
- val parcelFileDescriptor =
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
- val fileDescriptor = parcelFileDescriptor.fileDescriptor
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
-
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- // b/168187087 if there is only VideoCapture , VideoCapture will failed when setting the
- // repeating request with the surface, the workaround is binding one more useCase
- // Preview.
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val outputFileOptions =
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(fileDescriptor).build()
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-
- // Start recording with FileDescriptor
- videoCapture.startRecording(
- outputFileOptions,
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- // Recording for seconds
- recordingUntilKeyFrameArrived(videoCapture)
-
- // Stop recording
- videoCapture.stopRecording()
-
- verify(callback, timeout(10000)).onVideoSaved(any())
- parcelFileDescriptor.close()
- file.delete()
- }
-
- @FlakyTest // b/182165222
- @Test(timeout = 30000)
- fun unbind_shouldStopRecording() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val outputFileOptions =
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build()
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
-
- videoCapture.startRecording(
- outputFileOptions,
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- recordingUntilKeyFrameArrived(videoCapture)
-
- instrumentation.runOnMainSync {
- cameraUseCaseAdapter.removeUseCases(listOf(videoCapture, preview))
- }
-
- verify(callback, timeout(10000)).onVideoSaved(any())
- file.delete()
- }
-
- @Test(timeout = 30000)
- @SdkSuppress(minSdkVersion = 26)
- fun startRecordingWithUri_whenAPILevelLargerThan26() {
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- getNewVideoOutputFileOptions(contentResolver),
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
- recordingUntilKeyFrameArrived(videoCapture)
-
- videoCapture.stopRecording()
-
- // Assert: Wait for the signal that the image has been saved.
- val outputFileResultsArgumentCaptor =
- ArgumentCaptor.forClass(
- androidx.camera.core.VideoCapture.OutputFileResults::class.java
- )
- verify(callback, timeout(10000)).onVideoSaved(outputFileResultsArgumentCaptor.capture())
-
- // get file path to remove it
- val saveLocationUri =
- outputFileResultsArgumentCaptor.value.savedUri
- assertThat(saveLocationUri).isNotNull()
-
- // Remove temp test file
- contentResolver.delete(saveLocationUri!!, null, null)
- }
-
- @Test(timeout = 30000)
- fun videoCapture_saveResultToFile() {
- val file = File.createTempFile("CameraX", ".tmp").apply {
- deleteOnExit()
- }
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val callback = mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build(),
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- recordingUntilKeyFrameArrived(videoCapture)
- videoCapture.stopRecording()
-
- // Wait for the signal that the video has been saved.
- verify(callback, timeout(10000)).onVideoSaved(any())
- file.delete()
- }
-
- /** Return a VideoOutputFileOption which is used to save a video. */
- private fun getNewVideoOutputFileOptions(
- resolver: ContentResolver
- ): androidx.camera.core.VideoCapture.OutputFileOptions {
- val videoFileName = "video_" + System.currentTimeMillis()
- val contentValues = ContentValues().apply {
- put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
- put(MediaStore.Video.Media.TITLE, videoFileName)
- put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName)
- }
-
- return androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
- resolver,
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues
- ).build()
- }
-
- private fun getSurfaceProvider(): Preview.SurfaceProvider {
- return createSurfaceTextureProvider(object : SurfaceTextureCallback {
- override fun onSurfaceTextureReady(surfaceTexture: SurfaceTexture, resolution: Size) {
- // No-op
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- surfaceTexture.release()
- }
- })
- }
-
- private fun recordingUntilKeyFrameArrived(videoCapture: androidx.camera.core.VideoCapture) {
- Logger.i(TAG, "recordingUntilKeyFrameArrived begins: " + System.nanoTime() / 1000)
- while (true) {
- if (videoCapture.mIsFirstVideoKeyFrameWrite.get() && videoCapture
- .mIsFirstAudioSampleWrite.get()
- ) {
- Logger.i(
- TAG,
- "Video Key Frame and audio frame Arrived: " + System.nanoTime() / 1000
- )
- break
- }
- Thread.sleep(100)
- }
- Logger.i(TAG, "recordingUntilKeyFrameArrived ends: " + System.nanoTime() / 1000)
- }
-}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt
deleted file mode 100644
index 64a717c..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTestWithoutAudioPermissionTest.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.Context
-import android.content.pm.PackageManager
-import android.graphics.SurfaceTexture
-import android.media.MediaMetadataRetriever
-import android.net.Uri
-import android.os.Build
-import android.util.Size
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Logger
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.core.content.ContextCompat
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 21)
-class VideoCaptureTestWithoutAudioPermissionTest {
- companion object {
- const val TAG: String = "VideoCaptureTestWithoutAudioPermission"
- }
- @get:Rule
- val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
- )
-
- @get:Rule
- val permissionRule: GrantPermissionRule =
- GrantPermissionRule.grant(
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- // Don't grant Manifest.permission.RECORD_AUDIO
- )
-
- private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
- private val context = ApplicationProvider.getApplicationContext<Context>()
-
- private lateinit var cameraSelector: CameraSelector
-
- private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-
- private lateinit var contentResolver: ContentResolver
-
- @Before
- fun setUp() {
- // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
- Assume.assumeFalse(
- "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
- Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
- )
-
- assumeTrue(CameraUtil.deviceHasCamera())
-
- cameraSelector = if (CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
- CameraSelector.DEFAULT_BACK_CAMERA
- } else {
- CameraSelector.DEFAULT_FRONT_CAMERA
- }
-
- CameraXUtil.initialize(
- context,
- Camera2Config.defaultConfig()
- ).get()
- cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
-
- contentResolver = context.contentResolver
- }
-
- @After
- fun tearDown() {
- instrumentation.runOnMainSync {
- if (this::cameraUseCaseAdapter.isInitialized) {
- cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
- }
- }
-
- CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
- }
-
- /**
- * This test intends to test recording features without audio permission (RECORD_AUDIO).
- * Currently we cannot guarantee test cases' running sequence, the audio permission might be
- * granted by previous tests.
- * And if we revoke audio permission on the runtime it will cause the test crash.
- * That makes it necessary to check if the audio permission is denied or not before the test.
- * It's conceivable this test will be skipped because it's not the first case to test.
- */
- @Test
- @Suppress("DEPRECATION")
- fun videoCapture_saveResultToFileWithoutAudioPermission() {
- val checkPermissionResult =
- ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
-
- Logger.i(TAG, "checkSelfPermission RECORD_AUDIO: $checkPermissionResult")
-
- // This test is only for audio permission does not granted case.
- assumeTrue(checkPermissionResult == PackageManager.PERMISSION_DENIED)
-
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
- val preview = Preview.Builder().build()
- val videoCapture = androidx.camera.core.VideoCapture.Builder().build()
-
- assumeTrue(
- "This combination (videoCapture, preview) is not supported.",
- cameraUseCaseAdapter.isUseCasesCombinationSupported(videoCapture, preview)
- )
- instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- getSurfaceProvider()
- )
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
- }
-
- val callback =
- Mockito.mock(androidx.camera.core.VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- androidx.camera.core.VideoCapture.OutputFileOptions.Builder(file).build(),
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
-
- Thread.sleep(3000)
-
- videoCapture.stopRecording()
-
- // Wait for the signal that the video has been saved.
- Mockito.verify(callback, Mockito.timeout(10000)).onVideoSaved(ArgumentMatchers.any())
-
- val mediaRetriever = MediaMetadataRetriever()
-
- mediaRetriever.apply {
- setDataSource(context, Uri.fromFile(file))
- val hasAudio = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
- val numOfTracks = extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS)
-
- // In most of case and test environment, the RECORD_AUDIO permission is granted.
- // But if there is any audio permission denied cases, the recording should be keeps
- // going and only video recorded.
- assertThat(hasAudio).isNull()
- assertThat(numOfTracks).isEqualTo("1")
- }
-
- file.delete()
- }
-
- private fun getSurfaceProvider(): Preview.SurfaceProvider {
- return SurfaceTextureProvider.createSurfaceTextureProvider(object :
- SurfaceTextureProvider.SurfaceTextureCallback {
- override fun onSurfaceTextureReady(
- surfaceTexture: SurfaceTexture,
- resolution: Size,
- ) {
- // No-op
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- surfaceTexture.release()
- }
- }
- )
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index 29cce4f..980b398 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -80,7 +80,6 @@
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
@@ -383,33 +382,6 @@
== CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE);
}
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseConfigsVideoCapture() {
- Collection<UseCaseConfig<?>> useCaseConfigs = new ArrayList<>();
- VideoCaptureConfig videoCaptureConfig =
- new VideoCaptureConfig(MutableOptionsBundle.create());
- useCaseConfigs.add(videoCaptureConfig);
- assertTrue(StreamUseCaseUtil.getStreamUseCaseFromUseCaseConfigs(useCaseConfigs,
- new ArrayList<>())
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD);
- }
-
- @SdkSuppress(minSdkVersion = 33)
- @Test
- public void getStreamUseCaseFromUseCaseConfigsVideoAndImageCapture() {
- Collection<UseCaseConfig<?>> useCaseConfigs = new ArrayList<>();
- VideoCaptureConfig videoCaptureConfig =
- new VideoCaptureConfig(MutableOptionsBundle.create());
- useCaseConfigs.add(videoCaptureConfig);
- ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig(
- MutableOptionsBundle.create());
- useCaseConfigs.add(imageCaptureConfig);
- assertTrue(StreamUseCaseUtil.getStreamUseCaseFromUseCaseConfigs(useCaseConfigs,
- new ArrayList<>())
- == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL);
- }
-
// Sharing surface of YUV format is supported since API 28
@SdkSuppress(minSdkVersion = 28)
@Test
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
index 602b86b..24d785a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
@@ -91,7 +91,7 @@
*/
@LargeTest
@RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = 23)
+@SdkSuppress(minSdkVersion = 28) // ImageWriter to PRIVATE format requires API 28
class ProcessingCaptureSessionTest(
private var lensFacing: Int,
// The pair specifies (Output image format to Input image format). SessionProcessor will
@@ -111,7 +111,7 @@
CameraSelector.LENS_FACING_BACK, (YUV_420_888 to YUV_420_888), (JPEG to null)
),
arrayOf(
- CameraSelector.LENS_FACING_BACK, (PRIVATE to null), (YUV_420_888 to YUV_420_888)
+ CameraSelector.LENS_FACING_BACK, (PRIVATE to null), (JPEG to YUV_420_888)
),
arrayOf(
CameraSelector.LENS_FACING_FRONT, (PRIVATE to null), (JPEG to null)
@@ -120,7 +120,7 @@
CameraSelector.LENS_FACING_FRONT, (YUV_420_888 to YUV_420_888), (JPEG to null)
),
arrayOf(
- CameraSelector.LENS_FACING_FRONT, (PRIVATE to null), (YUV_420_888 to YUV_420_888)
+ CameraSelector.LENS_FACING_FRONT, (PRIVATE to null), (JPEG to YUV_420_888)
)
)
}
@@ -310,8 +310,10 @@
)
// Assert
+ sessionProcessor.assertStartCaptureInvoked()
sessionConfigParameters.assertStillCaptureCompleted()
sessionConfigParameters.assertCaptureImageReceived()
+
val parametersConfig = sessionProcessor.getLatestParameters()
assertThat(
parametersConfig.isParameterSet(
@@ -323,6 +325,41 @@
).isTrue()
}
+ @Test
+ fun canIssueAfTrigger(): Unit = runBlocking(Dispatchers.Main) {
+ assertCanIssueTriggerRequest(CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START)
+ }
+
+ @Test
+ fun canIssueAePrecaptureTrigger(): Unit = runBlocking(Dispatchers.Main) {
+ assertCanIssueTriggerRequest(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+ }
+
+ private suspend fun <T : Any> assertCanIssueTriggerRequest(
+ testKey: CaptureRequest.Key<T>,
+ testValue: T
+ ) {
+ // Arrange
+ val cameraDevice = cameraDeviceHolder.get()!!
+ val captureSession = createProcessingCaptureSession()
+ captureSession.open(
+ sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
+ captureSessionOpenerBuilder.build()
+ ).awaitWithTimeout(3000)
+
+ // Act
+ captureSession.issueCaptureRequests(
+ listOf(sessionConfigParameters.getTriggerCaptureConfig(testKey, testValue))
+ )
+
+ // Assert
+ val triggerConfig = sessionProcessor.assertStartTriggerInvoked()
+ assertThat(triggerConfig.isParameterSet(testKey, testValue)).isTrue()
+ sessionConfigParameters.assertTriggerCompleted()
+ }
+
private fun <T> Config.isParameterSet(key: CaptureRequest.Key<T>, objValue: T): Boolean {
val options = CaptureRequestOptions.Builder.from(this).build()
return Objects.equals(
@@ -396,41 +433,6 @@
}
@Test
- fun willCancelRequests_whenIssueMultipleConfigs(): Unit = runBlocking(Dispatchers.Main) {
- // Arrange
- val cameraDevice = cameraDeviceHolder.get()!!
- val captureSession = createProcessingCaptureSession()
- captureSession.open(
- sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
- captureSessionOpenerBuilder.build()
- ).awaitWithTimeout(3000)
-
- val cancelCountLatch = CountDownLatch(2)
- val captureConfig1 = CaptureConfig.Builder().apply {
- templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
- addCameraCaptureCallback(object : CameraCaptureCallback() {
- override fun onCaptureCancelled() {
- cancelCountLatch.countDown()
- }
- })
- }.build()
- val captureConfig2 = CaptureConfig.Builder().apply {
- templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
- addCameraCaptureCallback(object : CameraCaptureCallback() {
- override fun onCaptureCancelled() {
- cancelCountLatch.countDown()
- }
- })
- }.build()
-
- // Act
- captureSession.issueCaptureRequests(listOf(captureConfig1, captureConfig2))
-
- // Assert
- assertThat(cancelCountLatch.await(3, TimeUnit.SECONDS)).isTrue()
- }
-
- @Test
fun willCancelNonStillCaptureRequests(): Unit = runBlocking(Dispatchers.Main) {
// Arrange
val cameraDevice = cameraDeviceHolder.get()!!
@@ -457,37 +459,6 @@
}
@Test
- fun willCancelRequests_whenPendingRequestNotFinished(): Unit = runBlocking(Dispatchers.Main) {
- // Arrange
- val cameraDevice = cameraDeviceHolder.get()!!
- val captureSession = createProcessingCaptureSession()
- captureSession.open(
- sessionConfigParameters.getSessionConfigForOpen(), cameraDevice,
- captureSessionOpenerBuilder.build()
- ).awaitWithTimeout(3000)
-
- // Act
- captureSession.issueCaptureRequests(
- listOf(sessionConfigParameters.getStillCaptureCaptureConfig())
- )
-
- val cancelCountLatch = CountDownLatch(1)
- val captureConfig = CaptureConfig.Builder().apply {
- templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
- addCameraCaptureCallback(object : CameraCaptureCallback() {
- override fun onCaptureCancelled() {
- cancelCountLatch.countDown()
- }
- })
- }.build()
- // send 2nd request immediately.
- captureSession.issueCaptureRequests(listOf(captureConfig))
-
- // Assert
- assertThat(cancelCountLatch.await(3, TimeUnit.SECONDS)).isTrue()
- }
-
- @Test
fun canExecuteStillCaptureOneByOne(): Unit = runBlocking(Dispatchers.Main) {
// Arrange
val cameraDevice = cameraDeviceHolder.get()!!
@@ -761,6 +732,7 @@
private val previewImageReady = CompletableDeferred<Unit>()
private val captureImageReady = CompletableDeferred<Unit>()
private val stillCaptureCompleted = CompletableDeferred<Unit>()
+ private val triggerRequestCompleted = CompletableDeferred<Unit>()
private val tagKey1 = "KEY1"
private val tagKey2 = "KEY2"
private val tagValue1 = "Value1"
@@ -890,6 +862,23 @@
}.build()
}
+ fun <T : Any> getTriggerCaptureConfig(
+ triggerKey: CaptureRequest.Key<T>,
+ triggerValue: T
+ ): CaptureConfig {
+ return CaptureConfig.Builder().apply {
+ templateType = CameraDevice.TEMPLATE_PREVIEW
+ implementationOptions = CaptureRequestOptions.Builder().apply {
+ setCaptureRequestOption(triggerKey, triggerValue)
+ }.build()
+ addCameraCaptureCallback(object : CameraCaptureCallback() {
+ override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
+ triggerRequestCompleted.complete(Unit)
+ }
+ })
+ }.build()
+ }
+
fun closeOutputSurfaces() {
previewOutputDeferrableSurface.close()
captureOutputDeferrableSurface.close()
@@ -921,6 +910,10 @@
captureImageReady.awaitWithTimeout(3000)
}
+ suspend fun assertTriggerCompleted() {
+ triggerRequestCompleted.awaitWithTimeout(3000)
+ }
+
fun tearDown() {
closeOutputSurfaces()
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
index aaef6b0..c7ef572 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
@@ -18,7 +18,6 @@
import android.content.Context
import android.graphics.ImageFormat
-import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
import android.os.Handler
import android.os.Looper
@@ -36,10 +35,15 @@
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
+import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.core.impl.CaptureProcessor
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.Identifier
import androidx.camera.core.impl.ImageProxyBundle
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -48,6 +52,7 @@
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CameraXUtil
import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeSessionProcessor
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -120,6 +125,7 @@
CameraXUtil.shutdown().get(10000, TimeUnit.MILLISECONDS)
}
+ @SdkSuppress(minSdkVersion = 28)
@Test
fun successCaptureImage_whenExtraYuvPrivYuvConfigurationSupported() = runBlocking {
var cameraSelector = createCameraSelectorById(cameraId)
@@ -152,14 +158,15 @@
// Image analysis use a YUV stream by default
var imageAnalysis = ImageAnalysis.Builder().build()
-
// Preview use a PRIV stream by default
var preview = Preview.Builder().build()
-
- // Forces the image capture to use a YUV stream
- var imageCapture =
- ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888).build()
-
+ var imageCapture = ImageCapture.Builder().build()
+ // This will force ImageCapture to use YUV_420_888 to configure capture session.
+ val fakeSessionProcessor = FakeSessionProcessor(
+ inputFormatPreview = null,
+ inputFormatCapture = ImageFormat.YUV_420_888
+ )
+ enableSessionProcessor(cameraUseCaseAdapter, fakeSessionProcessor)
withContext(Dispatchers.Main) {
preview.setSurfaceProvider(getSurfaceProvider())
cameraUseCaseAdapter.addUseCases(Arrays.asList(imageAnalysis, preview, imageCapture))
@@ -173,6 +180,7 @@
callback.awaitCapturesAndAssert()
}
+ @SdkSuppress(minSdkVersion = 28)
@Test
fun successCaptureImage_whenExtraYuvYuvYuvConfigurationSupported() = runBlocking {
var cameraSelector = createCameraSelectorById(cameraId)
@@ -205,15 +213,15 @@
// Image analysis use a YUV stream by default
var imageAnalysis = ImageAnalysis.Builder().build()
+ var preview = Preview.Builder().build()
+ var imageCapture = ImageCapture.Builder().build()
- // Sets a CaptureProcessor to make the preview use a YUV stream
- var preview = Preview.Builder()
- .setCaptureProcessor(FakePreviewCaptureProcessor()).build()
-
- // Forces the image capture to use a YUV stream
- var imageCapture =
- ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888).build()
-
+ // This will force ImageCapture / Preview to use YUV_420_888 to configure capture session.
+ val fakeSessionProcessor = FakeSessionProcessor(
+ inputFormatPreview = ImageFormat.YUV_420_888,
+ inputFormatCapture = ImageFormat.YUV_420_888
+ )
+ enableSessionProcessor(cameraUseCaseAdapter, fakeSessionProcessor)
withContext(Dispatchers.Main) {
preview.setSurfaceProvider(getSurfaceProvider())
cameraUseCaseAdapter.addUseCases(Arrays.asList(imageAnalysis, preview, imageCapture))
@@ -227,6 +235,31 @@
callback.awaitCapturesAndAssert()
}
+ private fun enableSessionProcessor(
+ cameraUseCaseAdapter: CameraUseCaseAdapter,
+ sessionProcessor: SessionProcessor
+ ) {
+ cameraUseCaseAdapter.setExtendedConfig(object : CameraConfig {
+ override fun getConfig(): Config {
+ return MutableOptionsBundle.create()
+ }
+
+ override fun getCompatibilityId(): Identifier {
+ return Identifier.create(0)
+ }
+
+ override fun getSessionProcessor(
+ valueIfMissing: SessionProcessor?
+ ): SessionProcessor? {
+ return sessionProcessor
+ }
+
+ override fun getSessionProcessor(): SessionProcessor {
+ return sessionProcessor
+ }
+ })
+ }
+
private fun createCameraSelectorById(id: String): CameraSelector {
var builder = CameraSelector.Builder()
@@ -247,19 +280,9 @@
}
private fun getSurfaceProvider(): Preview.SurfaceProvider {
- return SurfaceTextureProvider.createSurfaceTextureProvider(object :
- SurfaceTextureProvider.SurfaceTextureCallback {
- override fun onSurfaceTextureReady(
- surfaceTexture: SurfaceTexture,
- resolution: Size
- ) {
- // No-op
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- surfaceTexture.release()
- }
- })
+ // Must use auto draining SurfaceTexture which will close the Image. Otherwise it could
+ // block the imageWriter to cause problems.
+ return SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider()
}
/**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index 173093a..e530e40 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -100,7 +100,7 @@
private ProcessorState mProcessorState;
private static List<DeferrableSurface> sHeldProcessorSurfaces = new ArrayList<>();
@Nullable
- private volatile CaptureConfig mPendingCaptureConfig = null;
+ private volatile List<CaptureConfig> mPendingCaptureConfigs = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
volatile boolean mIsExecutingStillCaptureRequest = false;
private final SessionProcessorCaptureCallback mSessionProcessorCaptureCallback;
@@ -280,33 +280,69 @@
}
}
- private boolean isStillCapture(@NonNull List<CaptureConfig> captureConfigs) {
- if (captureConfigs.isEmpty()) {
- return false;
- }
- for (CaptureConfig captureConfig : captureConfigs) {
- // Don't need to consider TEMPLATE_VIDEO_SNAPSHOT case since extensions does not
- // support Video Capture yet
- if (captureConfig.getTemplateType() != CameraDevice.TEMPLATE_STILL_CAPTURE) {
- return false;
+ /**
+ * Send a trigger request. Currently only CONTROL_AF_TRIGGER and CONTROL_AE_PRECAPTURE_TRIGGER
+ * are supported.
+ */
+ void issueTriggerRequest(@NonNull CaptureConfig captureConfig) {
+ Logger.d(TAG, "issueTriggerRequest");
+ CaptureRequestOptions options =
+ CaptureRequestOptions.Builder.from(
+ captureConfig.getImplementationOptions()).build();
+
+ boolean hasTriggerParameters = false;
+ for (Config.Option<?> option : options.listOptions()) {
+ @SuppressWarnings("unchecked")
+ CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+ if (key.equals(CaptureRequest.CONTROL_AF_TRIGGER)
+ || key.equals(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)) {
+ hasTriggerParameters = true;
+ break;
}
}
- return true;
+
+ if (!hasTriggerParameters) {
+ cancelRequests(Arrays.asList(captureConfig));
+ return;
+ }
+ mSessionProcessor.startTrigger(options, new SessionProcessor.CaptureCallback() {
+ @Override
+ public void onCaptureFailed(int captureSequenceId) {
+ mExecutor.execute(() -> {
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
+ CameraCaptureFailure.Reason.ERROR));
+ }
+ });
+ }
+
+ @Override
+ public void onCaptureSequenceCompleted(int captureSequenceId) {
+ mExecutor.execute(() -> {
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ cameraCaptureCallback.onCaptureCompleted(
+ new CameraCaptureResult.EmptyCameraCaptureResult());
+ }
+ });
+ }
+ });
}
/**
- * Submit a still capture request via
- * {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}.
+ * Submit a list of capture requests.
*
- * <p>The method is more restrictive than {@link CaptureSession#issueCaptureRequests(List)}.
- * Only one @link CaptureConfig} with {@link CameraDevice#TEMPLATE_STILL_CAPTURE} template is
- * allowed. If the captureConfigs contain multiple {@link CaptureConfig}s or the contained
- * {@link CaptureConfig} does not use {@link CameraDevice#TEMPLATE_STILL_CAPTURE}, all
- * captureConfigs will be cancelled immediately.
+ * <p>Capture requests using {@link CameraDevice#TEMPLATE_STILL_CAPTURE} are executed by.
+ * {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}. Other
+ * capture requests that trigger {@link CaptureRequest#CONTROL_AF_TRIGGER} or
+ * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER} are executed by
+ * {@link SessionProcessor#startTrigger(Config, SessionProcessor.CaptureCallback)}.
*
- * <p>Camera2 capture options in {@link CaptureConfig#getImplementationOptions()} will be
+ * <p>For still capture requests, Camera2 capture options in
+ * {@link CaptureConfig#getImplementationOptions()} will be
* merged with the options in {@link SessionConfig#getImplementationOptions()} set by
- * {@link #setSessionConfig(SessionConfig)}. The merged parameters set will be passed to
+ * {@link #setSessionConfig(SessionConfig)}. The merged parameters set is passed to
* {@link SessionProcessor#setParameters(Config)} but it is up to the implementation of the
* {@link SessionProcessor} to determine which options to apply.
*
@@ -314,103 +350,30 @@
* to invoke callbacks of {@link CaptureCallbackContainer} type due to lack of the access to
* the camera2 {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback}.
*
- * <p>Still capture requests are expected to arrive one at a time sequentially by upper layer.
- * Capture requests will be cancelled if previous request have not finished.
+ * <p>Although it allows concurrent capture requests to be submitted, the session processor
+ * might not support more than one capture request to execute at the same time. The session
+ * processor could fail the request immediately if it can't run multiple requests.
*/
@Override
public void issueCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
if (captureConfigs.isEmpty()) {
return;
}
- if (captureConfigs.size() > 1 || !isStillCapture(captureConfigs)) {
- cancelRequests(captureConfigs);
- return;
- }
- // Only allows one capture config at a time.
- if (mPendingCaptureConfig != null || mIsExecutingStillCaptureRequest) {
- cancelRequests(captureConfigs);
- return;
- }
-
- // captureConfigs should contain exactly one CaptureConfig.
- CaptureConfig captureConfig = captureConfigs.get(0);
Logger.d(TAG, "issueCaptureRequests (id=" + mInstanceId + ") + state =" + mProcessorState);
-
switch (mProcessorState) {
case UNINITIALIZED:
case SESSION_INITIALIZED:
- mPendingCaptureConfig = captureConfig;
-
+ mPendingCaptureConfigs = captureConfigs;
break;
case ON_CAPTURE_SESSION_STARTED:
- mIsExecutingStillCaptureRequest = true;
- CaptureRequestOptions.Builder builder =
- CaptureRequestOptions.Builder.from(
- captureConfig.getImplementationOptions());
-
- if (captureConfig.getImplementationOptions().containsOption(
- CaptureConfig.OPTION_ROTATION)) {
- builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
- captureConfig.getImplementationOptions().retrieveOption(
- CaptureConfig.OPTION_ROTATION));
+ for (CaptureConfig captureConfig : captureConfigs) {
+ if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_STILL_CAPTURE) {
+ issueStillCaptureRequest(captureConfig);
+ } else {
+ issueTriggerRequest(captureConfig);
+ }
}
-
- if (captureConfig.getImplementationOptions().containsOption(
- CaptureConfig.OPTION_JPEG_QUALITY)) {
- builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
- captureConfig.getImplementationOptions().retrieveOption(
- CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
- }
-
- mStillCaptureOptions = builder.build();
- updateParameters(mSessionOptions, mStillCaptureOptions);
- mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
- @Override
- public void onCaptureStarted(
- int captureSequenceId, long timestamp) {
- }
-
- @Override
- public void onCaptureProcessStarted(
- int captureSequenceId) {
- }
-
- @Override
- public void onCaptureFailed(
- int captureSequenceId) {
- mExecutor.execute(() -> {
- for (CameraCaptureCallback cameraCaptureCallback :
- captureConfig.getCameraCaptureCallbacks()) {
- cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
- CameraCaptureFailure.Reason.ERROR));
- }
- mIsExecutingStillCaptureRequest = false;
- });
- }
-
- @Override
- public void onCaptureSequenceCompleted(int captureSequenceId) {
- mExecutor.execute(() -> {
- for (CameraCaptureCallback cameraCaptureCallback :
- captureConfig.getCameraCaptureCallbacks()) {
- cameraCaptureCallback.onCaptureCompleted(
- new CameraCaptureResult.EmptyCameraCaptureResult());
- }
- mIsExecutingStillCaptureRequest = false;
- });
- }
-
- @Override
- public void onCaptureSequenceAborted(int captureSequenceId) {
- }
-
- @Override
- public void onCaptureCompleted(long timestamp, int captureSequenceId,
- @NonNull Map<CaptureResult.Key, Object> result) {
-
- }
- });
break;
case ON_CAPTURE_SESSION_ENDED:
case CLOSED:
@@ -420,6 +383,52 @@
break;
}
}
+ void issueStillCaptureRequest(@NonNull CaptureConfig captureConfig) {
+ CaptureRequestOptions.Builder builder =
+ CaptureRequestOptions.Builder.from(
+ captureConfig.getImplementationOptions());
+
+ if (captureConfig.getImplementationOptions().containsOption(
+ CaptureConfig.OPTION_ROTATION)) {
+ builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
+ captureConfig.getImplementationOptions().retrieveOption(
+ CaptureConfig.OPTION_ROTATION));
+ }
+
+ if (captureConfig.getImplementationOptions().containsOption(
+ CaptureConfig.OPTION_JPEG_QUALITY)) {
+ builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
+ captureConfig.getImplementationOptions().retrieveOption(
+ CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
+ }
+
+ mStillCaptureOptions = builder.build();
+ updateParameters(mSessionOptions, mStillCaptureOptions);
+ mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
+ @Override
+ public void onCaptureFailed(
+ int captureSequenceId) {
+ mExecutor.execute(() -> {
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
+ CameraCaptureFailure.Reason.ERROR));
+ }
+ });
+ }
+
+ @Override
+ public void onCaptureSequenceCompleted(int captureSequenceId) {
+ mExecutor.execute(() -> {
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ cameraCaptureCallback.onCaptureCompleted(
+ new CameraCaptureResult.EmptyCameraCaptureResult());
+ }
+ });
+ }
+ });
+ }
/**
* {@inheritDoc}
@@ -457,10 +466,9 @@
setSessionConfig(mSessionConfig);
}
- if (mPendingCaptureConfig != null) {
- List<CaptureConfig> pendingCaptureConfigList = Arrays.asList(mPendingCaptureConfig);
- mPendingCaptureConfig = null;
- issueCaptureRequests(pendingCaptureConfigList);
+ if (mPendingCaptureConfigs != null) {
+ issueCaptureRequests(mPendingCaptureConfigs);
+ mPendingCaptureConfigs = null;
}
}
@@ -479,8 +487,7 @@
@NonNull
@Override
public List<CaptureConfig> getCaptureConfigs() {
- return mPendingCaptureConfig != null ? Arrays.asList(mPendingCaptureConfig)
- : Collections.emptyList();
+ return mPendingCaptureConfigs != null ? mPendingCaptureConfigs : Collections.emptyList();
}
/**
@@ -489,12 +496,14 @@
@Override
public void cancelIssuedCaptureRequests() {
Logger.d(TAG, "cancelIssuedCaptureRequests (id=" + mInstanceId + ")");
- if (mPendingCaptureConfig != null) {
- for (CameraCaptureCallback cameraCaptureCallback :
- mPendingCaptureConfig.getCameraCaptureCallbacks()) {
- cameraCaptureCallback.onCaptureCancelled();
+ if (mPendingCaptureConfigs != null) {
+ for (CaptureConfig captureConfig : mPendingCaptureConfigs) {
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ cameraCaptureCallback.onCaptureCancelled();
+ }
}
- mPendingCaptureConfig = null;
+ mPendingCaptureConfigs = null;
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
index 17881be..7af6200 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
@@ -27,7 +27,6 @@
import androidx.camera.core.impl.PreviewConfig;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.VideoCaptureConfig;
import java.util.Collection;
@@ -89,15 +88,10 @@
}
- if (useCaseConfig instanceof VideoCaptureConfig) {
- if (hasImageCapture) {
- // If has both image and video capture, return preview video still case.
- return CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL;
- }
- hasVideoCapture = true;
- continue;
-
- }
+ // TODO: Need to handle "hasVideoCapture". The original statement was removed in
+ // aosp/2299682 because it uses the legacy core.VideoCapture API, which means the
+ // statement's content will never be run. The new video.VideoCapture API should
+ // used.
}
if (hasImageCapture) {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index 665019a..d10baed 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -16,8 +16,6 @@
package androidx.camera.camera2.internal;
-import static com.google.common.truth.Truth.assertThat;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -39,26 +37,20 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
-import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.CameraX;
import androidx.camera.core.CameraXConfig;
-import androidx.camera.core.ImageCapture;
import androidx.camera.core.InitializationException;
-import androidx.camera.core.Preview;
-import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceConfig.ConfigSize;
import androidx.camera.core.impl.SurfaceConfig.ConfigType;
-import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.CameraXUtil;
-import androidx.camera.testing.Configs;
import androidx.camera.testing.fakes.FakeCamera;
import androidx.camera.testing.fakes.FakeCameraFactory;
import androidx.test.core.app.ApplicationProvider;
@@ -77,10 +69,7 @@
import org.robolectric.shadows.ShadowCameraCharacteristics;
import org.robolectric.shadows.ShadowCameraManager;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -360,74 +349,6 @@
}
}
- @SuppressWarnings("deprecation")
- @Test(expected = IllegalArgumentException.class)
- public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
- ImageCapture imageCapture = new ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- androidx.camera.core.VideoCapture videoCapture =
- new androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- Preview preview = new Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
-
- List<UseCase> useCases = new ArrayList<>();
- useCases.add(imageCapture);
- useCases.add(videoCapture);
- useCases.add(preview);
-
- Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- mCameraFactory.getCamera(LEGACY_CAMERA_ID).getCameraInfoInternal(),
- useCases,
- mUseCaseConfigFactory);
- // A legacy level camera device can't support JPEG (ImageCapture) + PRIV (VideoCapture) +
- // PRIV (Preview) combination. An IllegalArgumentException will be thrown when trying to
- // bind these use cases at the same time.
- mSurfaceManager.getSuggestedResolutions(LEGACY_CAMERA_ID, Collections.emptyList(),
- new ArrayList<>(useCaseToConfigMap.values()));
- }
-
- @SuppressWarnings("deprecation")
- @Test
- public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
- ImageCapture imageCapture = new ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- androidx.camera.core.VideoCapture videoCapture =
- new androidx.camera.core.VideoCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
- Preview preview = new Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build();
-
- List<UseCase> useCases = new ArrayList<>();
- useCases.add(imageCapture);
- useCases.add(videoCapture);
- useCases.add(preview);
-
- Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- mCameraFactory.getCamera(LIMITED_CAMERA_ID).getCameraInfoInternal(),
- useCases,
- mUseCaseConfigFactory);
- Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
- mSurfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, Collections.emptyList(),
- new ArrayList<>(useCaseToConfigMap.values()));
-
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(imageCapture),
- mRecordSize);
- assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(videoCapture),
- mMaximumVideoSize);
- assertThat(suggestedResolutionMap).containsEntry(useCaseToConfigMap.get(preview),
- mPreviewSize);
- }
-
@Test
public void transformSurfaceConfigWithYUVAnalysisSize() {
SurfaceConfig surfaceConfig = mSurfaceManager.transformSurfaceConfig(
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 1d8366d5..c4a3072 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -2786,11 +2786,11 @@
}
/**
- * Creates [Preview], [ImageCapture], [ImageAnalysis], [androidx.camera.core.VideoCapture] or
- * FakeUseCase according to the specified settings.
+ * Creates [Preview], [ImageCapture], [ImageAnalysis] or FakeUseCase according to the specified
+ * settings.
*
- * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis],
- * [androidx.camera.core.VideoCapture] and FakeUseCase should be created.
+ * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis] and FakeUseCase should
+ * be created.
* @param targetRotation the target rotation setting. Default is UNKNOWN_ROTATION and no target
* rotation will be set to the created use case.
* @param targetAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
@@ -2802,7 +2802,6 @@
* @param defaultResolution the default resolution setting. Default is null.
* @param supportedResolutions the customized supported resolutions. Default is null.
*/
- @Suppress("DEPRECATION")
private fun createUseCaseByLegacyApi(
useCaseType: Int,
targetRotation: Int = UNKNOWN_ROTATION,
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 4eb4cb2..401ff46 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -23,6 +23,15 @@
public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
}
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ }
+
@RequiresApi(21) public interface CameraFilter {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
}
@@ -349,6 +358,10 @@
method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
}
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
method public abstract android.graphics.Rect getCropRect();
method public abstract android.util.Size getResolution();
@@ -360,6 +373,26 @@
ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
}
+ public interface SurfaceOutput {
+ method public void close();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ ctor public SurfaceOutput.Event();
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
@RequiresApi(21) public final class SurfaceRequest {
method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
method public void clearTransformationInfoListener();
@@ -397,12 +430,14 @@
}
@RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
method public androidx.camera.core.ViewPort? getViewPort();
}
public static final class UseCaseGroup.Builder {
ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
method public androidx.camera.core.UseCaseGroup build();
method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index 139d818..41025e7 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -23,6 +23,15 @@
public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
}
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ }
+
@RequiresApi(21) public interface CameraFilter {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
}
@@ -362,6 +371,10 @@
method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
}
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
method public abstract android.graphics.Rect getCropRect();
method public abstract android.util.Size getResolution();
@@ -373,6 +386,26 @@
ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
}
+ public interface SurfaceOutput {
+ method public void close();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ ctor public SurfaceOutput.Event();
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
@RequiresApi(21) public final class SurfaceRequest {
method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
method public void clearTransformationInfoListener();
@@ -410,12 +443,14 @@
}
@RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
method public androidx.camera.core.ViewPort? getViewPort();
}
public static final class UseCaseGroup.Builder {
ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
method public androidx.camera.core.UseCaseGroup build();
method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 4eb4cb2..401ff46 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -23,6 +23,15 @@
public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
}
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ }
+
@RequiresApi(21) public interface CameraFilter {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
}
@@ -349,6 +358,10 @@
method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
}
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
method public abstract android.graphics.Rect getCropRect();
method public abstract android.util.Size getResolution();
@@ -360,6 +373,26 @@
ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
}
+ public interface SurfaceOutput {
+ method public void close();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ ctor public SurfaceOutput.Event();
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
@RequiresApi(21) public final class SurfaceRequest {
method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
method public void clearTransformationInfoListener();
@@ -397,12 +430,14 @@
}
@RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
method public androidx.camera.core.ViewPort? getViewPort();
}
public static final class UseCaseGroup.Builder {
ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
method public androidx.camera.core.UseCaseGroup build();
method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
index 1ce7421..d1bcd62 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageProcessingUtilTest.java
@@ -18,6 +18,7 @@
import static androidx.camera.core.ImageProcessingUtil.convertJpegBytesToImage;
import static androidx.camera.core.ImageProcessingUtil.rotateYUV;
+import static androidx.camera.core.ImageProcessingUtil.writeJpegBytesToSurface;
import static androidx.camera.testing.ImageProxyUtil.createYUV420ImagePlanes;
import static com.google.common.truth.Truth.assertThat;
@@ -33,6 +34,7 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.camera.core.impl.utils.Exif;
import androidx.camera.testing.fakes.FakeImageInfo;
import androidx.camera.testing.fakes.FakeImageProxy;
import androidx.core.math.MathUtils;
@@ -47,6 +49,7 @@
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
@@ -151,6 +154,61 @@
assertBitmapColor(bitmap, Color.RED, JPEG_ENCODE_ERROR_TOLERANCE);
}
+ @Test
+ public void writeJpegToSurface_returnsTheSameImage() {
+ // Arrange: create a JPEG image with solid color.
+ byte[] inputBytes = createJpegBytesWithSolidColor(Color.RED);
+
+ // Act: acquire image and get the bytes.
+ writeJpegBytesToSurface(mJpegImageReaderProxy.getSurface(), inputBytes);
+
+ final ImageProxy imageProxy = mJpegImageReaderProxy.acquireLatestImage();
+ assertThat(imageProxy).isNotNull();
+ ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
+ byteBuffer.rewind();
+ byte[] outputBytes = new byte[byteBuffer.capacity()];
+ byteBuffer.get(outputBytes);
+
+ // Assert: the color and the dimension of the restored image.
+ Bitmap bitmap = BitmapFactory.decodeByteArray(outputBytes, 0, outputBytes.length);
+ assertThat(bitmap.getWidth()).isEqualTo(WIDTH);
+ assertThat(bitmap.getHeight()).isEqualTo(HEIGHT);
+ assertBitmapColor(bitmap, Color.RED, JPEG_ENCODE_ERROR_TOLERANCE);
+ }
+
+ @Test
+ public void convertYuvToJpegBytesIntoSurface_sizeAndRotationAreCorrect() throws IOException {
+ final int expectedRotation = 270;
+ // Arrange: create a YUV_420_888 image
+ mYUVImageProxy.setPlanes(createYUV420ImagePlanes(
+ WIDTH,
+ HEIGHT,
+ PIXEL_STRIDE_Y,
+ PIXEL_STRIDE_UV,
+ /*flipUV=*/false,
+ /*incrementValue=*/false));
+
+ // Act: convert it into JPEG and write into the surface.
+ ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(mYUVImageProxy,
+ 100, expectedRotation, mJpegImageReaderProxy.getSurface());
+
+ final ImageProxy imageProxy = mJpegImageReaderProxy.acquireLatestImage();
+ assertThat(imageProxy).isNotNull();
+ ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
+ byteBuffer.rewind();
+ byte[] outputBytes = new byte[byteBuffer.capacity()];
+ byteBuffer.get(outputBytes);
+
+ // Assert: the format is JPEG and can decode, the size is correct, the rotation in Exif
+ // is correct.
+ assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.JPEG);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(outputBytes, 0, outputBytes.length);
+ assertThat(bitmap.getWidth()).isEqualTo(WIDTH);
+ assertThat(bitmap.getHeight()).isEqualTo(HEIGHT);
+ Exif exif = Exif.createFromImageProxy(imageProxy);
+ assertThat(exif.getRotation()).isEqualTo(expectedRotation);
+ }
+
/**
* Returns JPEG bytes of a image with the given color.
*/
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
deleted file mode 100644
index 878f0c5..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.graphics.ImageFormat
-import android.graphics.Matrix
-import android.media.ImageReader
-import android.media.ImageWriter
-import android.os.Handler
-import androidx.camera.core.impl.ImageReaderProxy
-import androidx.camera.core.impl.MutableTagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ModifiableImageReaderProxyTest {
- private lateinit var imageReader: ImageReader
- private lateinit var imageReaderProxy: ModifiableImageReaderProxy
- private var imageWriter: ImageWriter? = null
-
- @Before
- fun setUp() {
- imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
- imageReaderProxy = ModifiableImageReaderProxy(imageReader)
- }
-
- @After
- fun tearDown() {
- imageReaderProxy.close()
- imageWriter?.close()
- }
-
- @Test
- fun canModifyImageTagBundle_acquireNext() {
- generateImage(imageReader)
-
- val tagBundle = MutableTagBundle.create()
- imageReaderProxy.setImageTagBundle(tagBundle)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
- }
-
- @Test
- fun canModifyImageTagBundle_acquireLatest() {
- generateImage(imageReader)
-
- val tagBundle = MutableTagBundle.create()
- imageReaderProxy.setImageTagBundle(tagBundle)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageTimestamp_acquireNext() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageTimeStamp(1000)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageTimestamp_acquireLatest() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageTimeStamp(1000)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageRotationDegrees_acquireNext() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageRotationDegrees(90)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageRotationDegress_acquireLatest() {
- generateImage(imageReader)
-
- imageReaderProxy.setImageRotationDegrees(90)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageMatrix_acquireNext() {
- generateImage(imageReader)
-
- val matrix = Matrix()
- imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
- val imageProxy = imageReaderProxy.acquireNextImage()
- assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
- imageProxy.close()
- }
-
- @Test
- fun canModifyImageMatrix_acquireLatest() {
- generateImage(imageReader)
-
- val matrix = Matrix()
- imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
- val imageProxy = imageReaderProxy.acquireLatestImage()
- assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
- imageProxy.close()
- }
-
- private fun generateImage(imageReader: ImageReader) {
- imageWriter = ImageWriter.newInstance(imageReader.surface, 2)
- val image = imageWriter!!.dequeueInputImage()
- imageWriter!!.queueInputImage(image)
- }
-
- @Test
- fun parametersMatchesInnerImageReader() {
- assertThat(imageReaderProxy.width).isEqualTo(640)
- assertThat(imageReaderProxy.height).isEqualTo(480)
- assertThat(imageReaderProxy.imageFormat).isEqualTo(ImageFormat.YUV_420_888)
- assertThat(imageReaderProxy.maxImages).isEqualTo(2)
- assertThat(imageReaderProxy.surface).isEqualTo(imageReader.surface)
- }
-
- @Test
- fun setOnImageAvailableListener_innerReaderIsInvoked() {
- val imageReader = Mockito.mock(
- ImageReader::class.java
- )
- val imageReaderProxy = ModifiableImageReaderProxy(imageReader)
-
- val listener = Mockito.mock(
- ImageReaderProxy.OnImageAvailableListener::class.java
- )
- imageReaderProxy.setOnImageAvailableListener(
- listener,
- CameraXExecutors.directExecutor()
- )
-
- val transformedListenerCaptor = ArgumentCaptor.forClass(
- ImageReader.OnImageAvailableListener::class.java
- )
- val handlerCaptor = ArgumentCaptor.forClass(
- Handler::class.java
- )
- Mockito.verify(imageReader, Mockito.times(1))
- .setOnImageAvailableListener(
- transformedListenerCaptor.capture(), handlerCaptor.capture()
- )
-
- transformedListenerCaptor.value.onImageAvailable(imageReader)
- Mockito.verify(listener, Mockito.times(1)).onImageAvailable(imageReaderProxy)
- }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
deleted file mode 100644
index 1b55484..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingSurfaceTest.kt
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.camera.core
-
-import android.graphics.ImageFormat
-import android.media.ImageWriter
-import android.os.Handler
-import android.os.HandlerThread
-import android.util.Pair
-import android.util.Size
-import android.view.Surface
-import androidx.annotation.RequiresApi
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.CaptureStage
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.ImageProxyBundle
-import androidx.camera.core.impl.ImmediateSurface
-import androidx.camera.core.impl.TagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.impl.utils.futures.Futures
-import androidx.camera.testing.fakes.FakeCameraCaptureResult
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.util.concurrent.ListenableFuture
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.Executors
-import java.util.concurrent.Semaphore
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ProcessingSurfaceTest {
- private var backgroundThread: HandlerThread? = null
- private var backgroundHandler: Handler? = null
- private val captureStage: CaptureStage = CaptureStage.DefaultCaptureStage()
- private val processingSurfaces: MutableList<ProcessingSurface> = ArrayList()
- private val captureProcessor: FakeCaptureProcessor = FakeCaptureProcessor(captureStage.id)
-
- @Before
- fun setup() {
- backgroundThread = HandlerThread("CallbackThread")
- backgroundThread!!.start()
- backgroundHandler = Handler(backgroundThread!!.looper)
- }
-
- @After
- fun tearDown() {
- for (processingSurface in processingSurfaces) {
- processingSurface.close()
- }
- processingSurfaces.clear()
- backgroundThread!!.looper.quitSafely()
- }
-
- @Test
- fun validInputSurface() {
- val processingSurface = createProcessingSurface(
- newImmediateSurfaceDeferrableSurface()
- )
- val surface = processingSurface.surface.get()
- assertThat(surface).isNotNull()
- }
-
- @Test
- fun writeToInputSurface_userOutputSurfaceReceivesFrame() {
- // Arrange.
- val frameReceivedSemaphore = Semaphore(0)
- val imageReaderProxy = ImageReaderProxys.createIsolatedReader(
- RESOLUTION.width, RESOLUTION.height,
- ImageFormat.YUV_420_888, 2
- )
- imageReaderProxy.setOnImageAvailableListener(
- { frameReceivedSemaphore.release() },
- CameraXExecutors.directExecutor()
- )
-
- // Create ProcessingSurface with user Surface.
- val processingSurface = createProcessingSurface(
- object : DeferrableSurface() {
- override fun provideSurface(): ListenableFuture<Surface> {
- return Futures.immediateFuture(imageReaderProxy.surface)
- }
- })
-
- // Act: Send one frame to processingSurface.
- triggerImage(processingSurface, 1)
-
- // Assert: verify that the frame has been received or time-out after 3 second.
- assertThat(frameReceivedSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
- }
-
- // Exception should be thrown here
- @Test
- fun getSurfaceThrowsExceptionWhenClosed() {
- val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
- processingSurface.close()
-
- // Exception should be thrown here
- val futureSurface = processingSurface.surface
- var cause: Throwable? = null
- try {
- futureSurface.get()
- } catch (e: ExecutionException) {
- cause = e.cause
- } catch (e: InterruptedException) {
- cause = e.cause
- }
- assertThat(cause).isInstanceOf(DeferrableSurface.SurfaceClosedException::class.java)
- }
-
- // Exception should be thrown here
- @Test(expected = IllegalStateException::class)
- fun getCameraCaptureCallbackThrowsExceptionWhenReleased() {
- val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
- processingSurface.close()
-
- // Exception should be thrown here
- processingSurface.cameraCaptureCallback
- }
-
- @Test
- fun completeTerminationFutureAfterProcessIsFinished() {
- // Arrange.
- val processingSurface = createProcessingSurface(newImmediateSurfaceDeferrableSurface())
-
- // Sets up the processor blocker to block the CaptureProcessor#process() function execution.
- captureProcessor.setupProcessorBlocker()
-
- // Monitors whether the ProcessingSurface termination future has been completed.
- val terminationCountDownLatch = CountDownLatch(1)
- processingSurface.terminationFuture.addListener({
- terminationCountDownLatch.countDown()
- }, Executors.newSingleThreadExecutor())
-
- // Act: Sends one frame to processingSurface.
- triggerImage(processingSurface, 1)
-
- // Waits for that the CaptureProcessor#process() function is called. Otherwise, the
- // following ProcessingSurface#close() function call may directly close the
- // ProcessingSurface.
- assertThat(captureProcessor.awaitProcessingState(1000, TimeUnit.MILLISECONDS)).isTrue()
-
- // Act: triggers the ProcessingSurface close flow.
- processingSurface.close()
-
- // Assert: verify that the termination future won't be completed before
- // CaptureProcessor#process() execution is finished.
- assertThat(terminationCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse()
-
- // Act: releases the processor blocker to finish the CaptureProcessor#process() execution.
- captureProcessor.releaseProcessorBlocker()
-
- // Assert: verify that the termination future is completed after CaptureProcessor#process()
- // execution is finished.
- assertThat(terminationCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
- }
-
- @RequiresApi(23)
- @Throws(ExecutionException::class, InterruptedException::class)
- private fun triggerImage(processingSurface: ProcessingSurface, timestamp: Long) {
- val surface = processingSurface.surface.get()
- val imageWriter = ImageWriter.newInstance(surface, 2)
- val image = imageWriter.dequeueInputImage()
- image.timestamp = timestamp
- imageWriter.queueInputImage(image)
- val callback = processingSurface.cameraCaptureCallback
- val cameraCaptureResult = FakeCameraCaptureResult()
- cameraCaptureResult.timestamp = timestamp
- cameraCaptureResult.setTag(
- TagBundle.create(
- Pair(Integer.toString(captureStage.hashCode()), captureStage.id)
- )
- )
- callback!!.onCaptureCompleted(cameraCaptureResult)
- }
-
- private fun createProcessingSurface(
- deferrableSurface: DeferrableSurface
- ): ProcessingSurface {
- val processingSurface = ProcessingSurface(
- RESOLUTION.width,
- RESOLUTION.height,
- FORMAT,
- backgroundHandler,
- captureStage,
- captureProcessor,
- deferrableSurface, Integer.toString(captureStage.hashCode())
- )
- processingSurfaces.add(processingSurface)
- return processingSurface
- }
-
- private fun newImmediateSurfaceDeferrableSurface(): DeferrableSurface {
- val imageReaderProxy = ImageReaderProxys.createIsolatedReader(
- RESOLUTION.width, RESOLUTION.height,
- ImageFormat.YUV_420_888, 2
- )
-
- val deferrableSurface = ImmediateSurface(imageReaderProxy.surface!!)
-
- deferrableSurface.terminationFuture.addListener(
- { imageReaderProxy.close() },
- CameraXExecutors.directExecutor()
- )
-
- return deferrableSurface
- }
-
- companion object {
- private val RESOLUTION: Size by lazy { Size(640, 480) }
- private const val FORMAT = ImageFormat.YUV_420_888
- }
-
- /**
- * Capture processor that can write out an empty image to exercise the pipeline.
- *
- * <p>The fake capture processor can be controlled to be blocked in the processing state and
- * then release the blocker to complete it.
- */
- @RequiresApi(23)
- private class FakeCaptureProcessor(private val captureStageId: Int) : CaptureProcessor {
- private val processingCountDownLatch = CountDownLatch(1)
- private var processorBlockerCountDownLatch: CountDownLatch? = null
-
- var imageWriter: ImageWriter? = null
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {
- imageWriter = ImageWriter.newInstance(surface, 2)
- }
-
- override fun process(bundle: ImageProxyBundle) {
- processingCountDownLatch.countDown()
- try {
- val imageProxyListenableFuture = bundle.getImageProxy(captureStageId)
- val imageProxy = imageProxyListenableFuture[100, TimeUnit.MILLISECONDS]
- val image = imageWriter!!.dequeueInputImage()
- image.timestamp = imageProxy.imageInfo.timestamp
- imageWriter!!.queueInputImage(image)
-
- processorBlockerCountDownLatch?.await()
- } catch (_: ExecutionException) {
- } catch (_: TimeoutException) {
- } catch (_: InterruptedException) {
- }
- }
-
- override fun onResolutionUpdate(size: Size) {}
-
- fun awaitProcessingState(timeout: Long, timeUnit: TimeUnit): Boolean {
- return processingCountDownLatch.await(timeout, timeUnit)
- }
-
- fun setupProcessorBlocker() {
- if (processorBlockerCountDownLatch == null) {
- processorBlockerCountDownLatch = CountDownLatch(1)
- }
- }
-
- fun releaseProcessorBlocker() {
- processorBlockerCountDownLatch?.countDown()
- }
- }
-}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
index 9a165a8..38961da6 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/SessionConfigTest.java
@@ -369,17 +369,11 @@
return deferrableSurface;
}
- @SuppressWarnings("deprecation")
@Test
public void combineTwoSessionsSurfaces() {
DeferrableSurface previewSurface = createSurface(Preview.class);
- DeferrableSurface videoSurface = createSurface(androidx.camera.core.VideoCapture.class);
DeferrableSurface imageCaptureSurface = createSurface(ImageCapture.class);
- SessionConfig.Builder builder0 = new SessionConfig.Builder();
- builder0.addSurface(videoSurface);
- builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
-
SessionConfig.Builder builder1 = new SessionConfig.Builder();
builder1.addSurface(previewSurface);
builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
@@ -389,7 +383,6 @@
builder2.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
SessionConfig.ValidatingBuilder validatingBuilder = new SessionConfig.ValidatingBuilder();
- validatingBuilder.add(builder0.build());
validatingBuilder.add(builder1.build());
validatingBuilder.add(builder2.build());
@@ -397,8 +390,7 @@
List<DeferrableSurface> surfaces = sessionConfig.getSurfaces();
// Ensures the surfaces are all added and sorted correctly.
- assertThat(surfaces)
- .containsExactly(previewSurface, imageCaptureSurface, videoSurface).inOrder();
+ assertThat(surfaces).containsExactly(previewSurface, imageCaptureSurface).inOrder();
}
@Test
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
index 8fad00e..bf3864f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
@@ -17,8 +17,6 @@
import static androidx.core.util.Preconditions.checkArgument;
-import android.os.Build;
-
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,21 +30,49 @@
/**
* An effect for one or multiple camera outputs.
*
- * <p>A {@link CameraEffect} class contains 2 types of information, the processor and the
- * configuration.
+ * <p>This API allows the implementer to inject code into CameraX pipeline and apply visual
+ * effects, such as a portrait effect. The {@link CameraEffect} class contains two types of
+ * information, the processor and the configuration.
* <ul>
- * <li> The processor is an implementation of either {@link SurfaceProcessor} or
+ * <li>The processor is an implementation of either {@link SurfaceProcessor} or
* {@link ImageProcessor}. It consumes original camera frames from CameraX, applies the effect,
* and returns the processed frames back to CameraX.
- * <li> The configuration provides information on how the processor should be injected into the
- * pipeline. For example, the target {@link UseCase}s where the effect should be applied.
+ * <li>The configuration provides information on how the processor should be injected into the
+ * pipeline. For example, the target {@link UseCase}s where the effect should be applied. It may
+ * also contain information about camera configuration. For example, the exposure level.
* </ul>
*
- * @hide
+ * <p>If CameraX fails to send frames to the {@link CameraEffect}, the error will be
+ * delivered to the app via error callbacks such as
+ * {@link ImageCapture.OnImageCapturedCallback#onError}. If {@link CameraEffect} fails to
+ * process and return the frames, for example, unable to allocate the resources for image
+ * processing, it must throw {@link Exception} in the processor implementation. The
+ * {@link Exception} will be caught and forwarded to the app via error callbacks. Please see the
+ * Javadoc of the processor interfaces for details.
+ *
+ * <p>Extend this class to create specific effects. The {@link Executor} provided in the
+ * constructors will be used by CameraX to call the processors.
+ *
+ * <p>Code sample for a portrait effect that targets the {@link Preview} {@link UseCase}:
+ *
+ * <pre><code>
+ * class PortraitPreviewEffect extends CameraEffect {
+ * PortraitPreviewEffect() {
+ * super(PREVIEW, getExecutor(), getSurfaceProcessor());
+ * }
+ *
+ * private static Executor getExecutor() {
+ * // Returns an executor for calling the SurfaceProcessor
+ * }
+ *
+ * private static SurfaceProcessor getSurfaceProcessor() {
+ * // Return a SurfaceProcessor implementation that applies a portrait effect.
+ * }
+ * }
+ * </code></pre>
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class CameraEffect {
+@RequiresApi(21)
+public abstract class CameraEffect {
/**
* Bitmask options for the effect targets.
@@ -54,8 +80,8 @@
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- @IntDef(flag = true, value = {PREVIEW, VIDEO_CAPTURE, IMAGE_CAPTURE})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(flag = true, value = {PREVIEW, IMAGE_CAPTURE})
public @interface Targets {
}
@@ -66,7 +92,10 @@
/**
* Bitmask option to indicate that CameraX should apply this effect to {@code VideoCapture}.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int VIDEO_CAPTURE = 1 << 1;
/**
@@ -93,7 +122,9 @@
* @param imageProcessor a {@link ImageProcessor} implementation. Once the effect is active,
* CameraX will send frames to the {@link ImageProcessor} on the
* {@param executor}, and deliver the processed frames to the app.
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
protected CameraEffect(
@Targets int targets,
@NonNull Executor executor,
@@ -141,7 +172,7 @@
/**
* Gets the {@link Executor} associated with this effect.
*
- * <p>This method returns the value set in {@link CameraEffect}'s constructor.
+ * <p>This method returns the value set in the constructor.
*/
@NonNull
public Executor getExecutor() {
@@ -158,8 +189,11 @@
/**
* Gets the {@link ImageProcessor} associated with this effect.
+ *
+ * @hide
*/
@Nullable
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public ImageProcessor getImageProcessor() {
return mImageProcessor;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CaptureProcessorPipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/CaptureProcessorPipeline.java
deleted file mode 100644
index 2a832e6..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/CaptureProcessorPipeline.java
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.ImageFormat;
-import android.media.ImageReader;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.core.impl.ImageReaderProxy;
-import androidx.camera.core.impl.TagBundle;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-
-/**
- * A CaptureProcessor which can link two CaptureProcessors.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class CaptureProcessorPipeline implements CaptureProcessor {
- private static final String TAG = "CaptureProcessorPipeline";
- @NonNull
- private final CaptureProcessor mPreCaptureProcessor;
- @NonNull
- private final CaptureProcessor mPostCaptureProcessor;
- @NonNull
- private final ListenableFuture<List<Void>> mUnderlyingCaptureProcessorsCloseFuture;
- @NonNull
- final Executor mExecutor;
- private final int mMaxImages;
- private ImageReaderProxy mIntermediateImageReader = null;
- private ImageInfo mSourceImageInfo = null;
-
- private final Object mLock = new Object();
-
- @GuardedBy("mLock")
- private boolean mClosed = false;
-
- @GuardedBy("mLock")
- private boolean mProcessing = false;
-
- @GuardedBy("mLock")
- CallbackToFutureAdapter.Completer<Void> mCloseCompleter;
- @GuardedBy("mLock")
- private ListenableFuture<Void> mCloseFuture;
-
- /**
- * Creates a {@link CaptureProcessorPipeline} to link two CaptureProcessors to process the
- * captured images.
- *
- * @param preCaptureProcessor The pre-processing {@link CaptureProcessor} which must output
- * YUV_420_888 {@link ImageProxy} for the post-processing
- * {@link CaptureProcessor} to process.
- * @param maxImages the maximum image buffer count used to create the intermediate
- * {@link ImageReaderProxy} to receive the processed
- * {@link ImageProxy} from the
- * pre-processing {@link CaptureProcessor}.
- * @param postCaptureProcessor The post-processing {@link CaptureProcessor} which can process
- * an {@link ImageProxy} of YUV_420_888 format . It must be able
- * to process the image without referencing to the
- * {@link TagBundle} and capture id.
- * @param executor the {@link Executor} used by the post-processing
- * {@link CaptureProcessor}
- * to process the {@link ImageProxy}.
- */
- CaptureProcessorPipeline(@NonNull CaptureProcessor preCaptureProcessor, int maxImages,
- @NonNull CaptureProcessor postCaptureProcessor, @NonNull Executor executor) {
- mPreCaptureProcessor = preCaptureProcessor;
- mPostCaptureProcessor = postCaptureProcessor;
-
- List<ListenableFuture<Void>> closeFutureList = new ArrayList<>();
- closeFutureList.add(mPreCaptureProcessor.getCloseFuture());
- closeFutureList.add(mPostCaptureProcessor.getCloseFuture());
- mUnderlyingCaptureProcessorsCloseFuture = Futures.allAsList(closeFutureList);
-
- mExecutor = executor;
- mMaxImages = maxImages;
- }
-
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
- // Updates the output surface to the post-processing CaptureProcessor
- mPostCaptureProcessor.onOutputSurface(surface, imageFormat);
- }
-
- @Override
- public void process(@NonNull ImageProxyBundle bundle) {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- mProcessing = true;
- }
-
- List<Integer> ids = bundle.getCaptureIds();
- ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(ids.get(0));
- Preconditions.checkArgument(imageProxyListenableFuture.isDone());
-
- ImageProxy imageProxy;
- try {
- imageProxy = imageProxyListenableFuture.get();
- ImageInfo imageInfo = imageProxy.getImageInfo();
- // Stores the imageInfo of source image that will be used when when the processed
- // ImageProxy is received from the pre-processing CaptureProcessor.
- mSourceImageInfo = imageInfo;
- } catch (ExecutionException | InterruptedException e) {
- throw new IllegalArgumentException("Can not successfully extract ImageProxy from the "
- + "ImageProxyBundle.");
- }
-
- // Calls the pre-processing CaptureProcessor to process the ImageProxyBundle
- mPreCaptureProcessor.process(bundle);
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
- // Creates an intermediate ImageReader to receive the processed image from the
- // pre-processing CaptureProcessor.
- mIntermediateImageReader = new AndroidImageReaderProxy(
- ImageReader.newInstance(size.getWidth(), size.getHeight(),
- ImageFormat.YUV_420_888, mMaxImages));
- mPreCaptureProcessor.onOutputSurface(mIntermediateImageReader.getSurface(),
- ImageFormat.YUV_420_888);
- mPreCaptureProcessor.onResolutionUpdate(size);
-
- // Updates the resolution information to the post-processing CaptureProcessor.
- mPostCaptureProcessor.onResolutionUpdate(size);
-
- // Register the ImageAvailableListener to receive the processed image from the
- // pre-processing CaptureProcessor.
- mIntermediateImageReader.setOnImageAvailableListener(
- imageReader -> {
- ImageProxy image = imageReader.acquireNextImage();
- try {
- mExecutor.execute(() -> postProcess(image));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "The executor for post-processing might have been "
- + "shutting down or terminated!");
- image.close();
- }
- },
- CameraXExecutors.directExecutor());
- }
-
- void postProcess(ImageProxy imageProxy) {
- boolean closed;
-
- synchronized (mLock) {
- closed = mClosed;
- }
-
- if (!closed) {
- Size resolution = new Size(imageProxy.getWidth(), imageProxy.getHeight());
-
- // Retrieves information from ImageInfo of source image to create a
- // SettableImageProxyBundle and calls the post-processing CaptureProcessor to process
- // it.
- Preconditions.checkNotNull(mSourceImageInfo);
- String tagBundleKey = mSourceImageInfo.getTagBundle().listKeys().iterator().next();
- int captureId = (Integer) mSourceImageInfo.getTagBundle().getTag(tagBundleKey);
- SettableImageProxy settableImageProxy =
- new SettableImageProxy(imageProxy, resolution, mSourceImageInfo);
- mSourceImageInfo = null;
-
- SettableImageProxyBundle settableImageProxyBundle = new SettableImageProxyBundle(
- Collections.singletonList(captureId), tagBundleKey);
- settableImageProxyBundle.addImageProxy(settableImageProxy);
-
- try {
- mPostCaptureProcessor.process(settableImageProxyBundle);
- } catch (Exception e) {
- Logger.e(TAG, "Post processing image failed! " + e.getMessage());
- }
- }
-
- synchronized (mLock) {
- mProcessing = false;
- }
-
- closeAndCompleteFutureIfNecessary();
- }
-
- /**
- * Closes the objects generated when creating the {@link CaptureProcessorPipeline}.
- */
- @Override
- public void close() {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- mClosed = true;
- }
-
- mPreCaptureProcessor.close();
- mPostCaptureProcessor.close();
- closeAndCompleteFutureIfNecessary();
- }
-
- private void closeAndCompleteFutureIfNecessary() {
- boolean closed;
- boolean processing;
- CallbackToFutureAdapter.Completer<Void> closeCompleter;
-
- synchronized (mLock) {
- closed = mClosed;
- processing = mProcessing;
- closeCompleter = mCloseCompleter;
-
- if (closed && !processing) {
- mIntermediateImageReader.close();
- }
- }
-
- if (closed && !processing && closeCompleter != null) {
- // Complete the capture process pipeline's close future after the underlying pre and
- // post capture processors are closed.
- mUnderlyingCaptureProcessorsCloseFuture.addListener(() -> closeCompleter.set(null),
- CameraXExecutors.directExecutor());
- }
- }
-
- /**
- * Returns a future that will complete when the CaptureProcessorPipeline is actually closed.
- *
- * @return A future that signals when the CaptureProcessorPipeline is actually closed
- * (after all processing). Cancelling this future has no effect.
- */
- @NonNull
- @Override
- public ListenableFuture<Void> getCloseFuture() {
- ListenableFuture<Void> closeFuture;
- synchronized (mLock) {
- if (mClosed && !mProcessing) {
- // Everything should be closed but still need to wait for underlying capture
- // processors being closed.
- closeFuture = Futures.transform(mUnderlyingCaptureProcessorsCloseFuture,
- nullVoid -> null, CameraXExecutors.directExecutor());
- } else {
- if (mCloseFuture == null) {
- mCloseFuture = CallbackToFutureAdapter.getFuture(completer -> {
- // Should already be locked, but lock again to satisfy linter.
- synchronized (mLock) {
- mCloseCompleter = completer;
- }
- return "CaptureProcessorPipeline-close";
- });
- }
- closeFuture = Futures.nonCancellationPropagating(mCloseFuture);
- }
- }
- return closeFuture;
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index df23742..17254cc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -19,7 +19,6 @@
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_BUNDLE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_FLASH_MODE;
@@ -28,7 +27,6 @@
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IMAGE_READER_PROXY_PROVIDER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IO_EXECUTOR;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_JPEG_COMPRESSION_QUALITY;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
@@ -108,7 +106,6 @@
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.MutableTagBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
@@ -326,16 +323,6 @@
private CaptureConfig mCaptureConfig;
- /** The set of requests that will be sent to the camera for the final captured image. */
- private CaptureBundle mCaptureBundle;
- private int mMaxCaptureStages;
-
- /**
- * Processing that gets done to the mCaptureBundle to produce the final image that is produced
- * by {@link #takePicture(Executor, OnImageCapturedCallback)}
- */
- private CaptureProcessor mCaptureProcessor;
-
/**
* Whether the software JPEG pipeline will be used.
*/
@@ -420,77 +407,30 @@
};
} else if (isSessionProcessorEnabledInCurrentCamera()) {
ImageReaderProxy imageReader;
+ // SessionProcessor only outputs JPEG format.
if (getImageFormat() == ImageFormat.JPEG) {
- imageReader =
- new AndroidImageReaderProxy(ImageReader.newInstance(resolution.getWidth(),
- resolution.getHeight(), getImageFormat(), MAX_IMAGES));
- } else if (getImageFormat() == ImageFormat.YUV_420_888) { // convert it into Jpeg
- if (Build.VERSION.SDK_INT >= 26) {
- // Jpeg rotation / quality will be set to softwareJpegProcessor later in
- // ImageCaptureRequestProcessor.
- softwareJpegProcessor =
- new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
-
- ModifiableImageReaderProxy inputReader =
- new ModifiableImageReaderProxy(
- ImageReader.newInstance(resolution.getWidth(),
- resolution.getHeight(),
- ImageFormat.YUV_420_888,
- MAX_IMAGES));
-
- CaptureBundle captureBundle = CaptureBundles.singleDefaultCaptureBundle();
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
- inputReader,
- captureBundle,
- softwareJpegProcessor
- ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
-
- // Ensure the ImageProxy contains the same capture stage id expected from the
- // ProcessingImageReader.
- MutableTagBundle tagBundle = MutableTagBundle.create();
- // Implicit non-null type use for getCaptureStages().
- //noinspection ConstantConditions
- tagBundle.putTag(processingImageReader.getTagBundleKey(),
- captureBundle.getCaptureStages().get(0).getId());
- inputReader.setImageTagBundle(tagBundle);
-
- imageReader = processingImageReader;
- } else {
- throw new UnsupportedOperationException("Does not support API level < 26");
- }
+ // SessionProcessor can't guarantee that image and capture result have the same
+ // time stamp. Thus we can't use MetadataImageReader
+ imageReader = ImageReaderProxys.createIsolatedReader(resolution.getWidth(),
+ resolution.getHeight(), ImageFormat.JPEG, MAX_IMAGES);
+ mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
+ };
} else {
throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
}
- mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
- };
mImageReader = new SafeCloseImageReaderProxy(imageReader);
- } else if (mCaptureProcessor != null || mUseSoftwareJpeg) {
- // Capture processor set from configuration takes precedence over software JPEG.
- CaptureProcessor captureProcessor = mCaptureProcessor;
+ } else if (mUseSoftwareJpeg) {
+ CaptureProcessor captureProcessor;
int inputFormat = getImageFormat();
- int outputFormat = getImageFormat();
- if (mUseSoftwareJpeg) {
- // API check to satisfy linter
- if (Build.VERSION.SDK_INT >= 26) {
- Logger.i(TAG, "Using software JPEG encoder.");
-
- if (mCaptureProcessor != null) {
- softwareJpegProcessor = new YuvToJpegProcessor(getJpegQualityInternal(),
- mMaxCaptureStages);
- captureProcessor = new CaptureProcessorPipeline(
- mCaptureProcessor, mMaxCaptureStages, softwareJpegProcessor,
- mExecutor);
- } else {
- captureProcessor = softwareJpegProcessor =
- new YuvToJpegProcessor(getJpegQualityInternal(), mMaxCaptureStages);
- }
-
- outputFormat = ImageFormat.JPEG;
- } else {
- // Note: This should never be hit due to SDK_INT check before setting
- // useSoftwareJpeg.
- throw new IllegalStateException("Software JPEG only supported on API 26+");
- }
+ // API check to satisfy linter
+ if (Build.VERSION.SDK_INT >= 26) {
+ Logger.i(TAG, "Using software JPEG encoder.");
+ captureProcessor = softwareJpegProcessor =
+ new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
+ } else {
+ // Note: This should never be hit due to SDK_INT check before setting
+ // useSoftwareJpeg.
+ throw new IllegalStateException("Software JPEG only supported on API 26+");
}
// TODO: To allow user to use an Executor for the image processing.
@@ -498,10 +438,10 @@
resolution.getWidth(),
resolution.getHeight(),
inputFormat,
- mMaxCaptureStages,
- getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
+ MAX_IMAGES,
+ CaptureBundles.singleDefaultCaptureBundle(),
captureProcessor
- ).setPostProcessExecutor(mExecutor).setOutputFormat(outputFormat).build();
+ ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
@@ -671,13 +611,7 @@
@Override
protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
- if (builder.getUseCaseConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
- != null && Build.VERSION.SDK_INT >= 29) {
- // TODO: The API level check can be removed if the ImageWriterCompat issue on API
- // level 28 devices (b182363220/) can be resolved.
- Logger.i(TAG, "Requesting software JPEG due to a CaptureProcessor is set.");
- builder.getMutableConfig().insertOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, true);
- } else if (cameraInfo.getCameraQuirks().contains(
+ if (cameraInfo.getCameraQuirks().contains(
SoftwareJpegEncodingPreferredQuirk.class)) {
// Request software JPEG encoder if quirk exists on this device and the software JPEG
// option has not already been explicitly set.
@@ -699,15 +633,13 @@
Integer bufferFormat = builder.getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT,
null);
if (bufferFormat != null) {
- Preconditions.checkArgument(
- builder.getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
- == null,
- "Cannot set buffer format with CaptureProcessor defined.");
+ Preconditions.checkArgument(!(isSessionProcessorEnabledInCurrentCamera()
+ && bufferFormat != ImageFormat.JPEG),
+ "Cannot set non-JPEG buffer format with Extensions enabled.");
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
useSoftwareJpeg ? ImageFormat.YUV_420_888 : bufferFormat);
} else {
- if (builder.getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null
- || useSoftwareJpeg) {
+ if (useSoftwareJpeg) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
ImageFormat.YUV_420_888);
} else {
@@ -728,13 +660,6 @@
}
}
}
-
- Integer maxCaptureStages =
- builder.getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES);
- checkNotNull(maxCaptureStages,
- "Maximum outstanding image count must be at least 1");
- Preconditions.checkArgument(maxCaptureStages >= 1,
- "Maximum outstanding image count must be at least 1");
return builder.getUseCaseConfig();
}
@@ -1678,12 +1603,6 @@
CaptureConfig.Builder captureBuilder = CaptureConfig.Builder.createFrom(useCaseConfig);
mCaptureConfig = captureBuilder.build();
- // Retrieve camera specific settings.
- mCaptureProcessor = useCaseConfig.getCaptureProcessor(null);
- mMaxCaptureStages = useCaseConfig.getMaxCaptureStages(MAX_IMAGES);
- mCaptureBundle = useCaseConfig.getCaptureBundle(
- CaptureBundles.singleDefaultCaptureBundle());
-
// This will only be set to true if software JPEG was requested and
// enforceSoftwareJpegConstraints() hasn't removed the request.
mUseSoftwareJpeg = useCaseConfig.isSoftwareJpegEncoderRequested();
@@ -1746,29 +1665,7 @@
if (mProcessingImageReader != null) {
// If the Processor is provided, check if we have valid CaptureBundle and update
// ProcessingImageReader before actually issuing a take picture request.
- captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
- if (captureBundle == null) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "ImageCapture cannot set empty CaptureBundle."));
- }
-
- List<CaptureStage> captureStages = captureBundle.getCaptureStages();
- if (captureStages == null) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "ImageCapture has CaptureBundle with null capture stages"));
- }
-
- if (mCaptureProcessor == null && captureStages.size() > 1) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "No CaptureProcessor can be found to process the images captured for "
- + "multiple CaptureStages."));
- }
-
- if (captureStages.size() > mMaxCaptureStages) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "ImageCapture has CaptureStages > Max CaptureStage size"));
- }
-
+ captureBundle = CaptureBundles.singleDefaultCaptureBundle();
mProcessingImageReader.setCaptureBundle(captureBundle);
mProcessingImageReader.setOnProcessingErrorCallback(
CameraXExecutors.directExecutor(),
@@ -1779,7 +1676,7 @@
});
tagBundleKey = mProcessingImageReader.getTagBundleKey();
} else {
- captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
+ captureBundle = CaptureBundles.singleDefaultCaptureBundle();
if (captureBundle == null) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ImageCapture cannot set empty CaptureBundle."));
@@ -1837,15 +1734,6 @@
return submitStillCaptureRequest(captureConfigs);
}
- private CaptureBundle getCaptureBundle(CaptureBundle defaultCaptureBundle) {
- List<CaptureStage> captureStages = mCaptureBundle.getCaptureStages();
- if (captureStages == null || captureStages.isEmpty()) {
- return defaultCaptureBundle;
- }
-
- return CaptureBundles.createCaptureBundle(captureStages);
- }
-
/**
* ===== New architecture start =====
*
@@ -1905,10 +1793,6 @@
// Use old pipeline for advanced Extensions.
return false;
}
- if (mCaptureProcessor != null) {
- // Use old pipeline for basic Extensions.
- return false;
- }
if (getCaptureStageSize(config) > 1) {
// Use old pipeline for multiple stages capture.
return false;
@@ -2811,16 +2695,9 @@
// is done)
Integer bufferFormat = getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT, null);
if (bufferFormat != null) {
- Preconditions.checkArgument(
- getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) == null,
- "Cannot set buffer format with CaptureProcessor defined.");
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, bufferFormat);
} else {
- if (getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null) {
- getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
- } else {
- getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
- }
+ getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
}
ImageCaptureConfig imageCaptureConfig = getUseCaseConfig();
@@ -2838,13 +2715,6 @@
targetResolution.getHeight()));
}
- Integer maxCaptureStages =
- getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES);
- checkNotNull(maxCaptureStages,
- "Maximum outstanding image count must be at least 1");
- Preconditions.checkArgument(maxCaptureStages >= 1,
- "Maximum outstanding image count must be at least 1");
-
checkNotNull(getMutableConfig().retrieveOption(OPTION_IO_EXECUTOR,
CameraXExecutors.ioExecutor()), "The IO executor can't be null");
@@ -2914,20 +2784,6 @@
}
/**
- * Sets the {@link CaptureProcessor}.
- *
- * @param captureProcessor The requested capture processor for extension.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
- getMutableConfig().insertOption(OPTION_CAPTURE_PROCESSOR, captureProcessor);
- return this;
- }
-
- /**
* Sets the {@link ImageFormat} of the {@link ImageProxy} returned by the
* {@link ImageCapture.OnImageCapturedCallback}.
*
@@ -2948,20 +2804,6 @@
return this;
}
- /**
- * Sets the max number of {@link CaptureStage}.
- *
- * @param maxCaptureStages The max CaptureStage number.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setMaxCaptureStages(int maxCaptureStages) {
- getMutableConfig().insertOption(OPTION_MAX_CAPTURE_STAGES, maxCaptureStages);
- return this;
- }
-
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index e7d869d..c69d3ab 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -34,6 +34,7 @@
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.internal.compat.ImageWriterCompat;
+import androidx.camera.core.internal.utils.ImageUtil;
import androidx.core.util.Preconditions;
import java.nio.ByteBuffer;
@@ -94,6 +95,45 @@
}
/**
+ * Writes a JPEG bytes data as an Image into the Surface. Returns true if it succeeds and false
+ * otherwise.
+ */
+ public static boolean writeJpegBytesToSurface(
+ @NonNull Surface surface,
+ @NonNull byte[] jpegBytes) {
+ Preconditions.checkNotNull(jpegBytes);
+ Preconditions.checkNotNull(surface);
+
+ if (nativeWriteJpegToSurface(jpegBytes, surface) != 0) {
+ Logger.e(TAG, "Failed to enqueue JPEG image.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Convert a YUV_420_888 ImageProxy to a JPEG bytes data as an Image into the Surface.
+ *
+ * <p>Returns true if it succeeds and false otherwise.
+ */
+ public static boolean convertYuvToJpegBytesIntoSurface(
+ @NonNull ImageProxy imageProxy,
+ @IntRange(from = 1, to = 100) int jpegQuality,
+ @ImageOutputConfig.RotationDegreesValue int rotationDegrees,
+ @NonNull Surface outputSurface) {
+ try {
+ byte[] jpegBytes =
+ ImageUtil.yuvImageToJpegByteArray(
+ imageProxy, null, jpegQuality, rotationDegrees);
+ return writeJpegBytesToSurface(outputSurface,
+ jpegBytes);
+ } catch (ImageUtil.CodecFailedException e) {
+ Logger.e(TAG, "Failed to encode YUV to JPEG", e);
+ return false;
+ }
+ }
+
+ /**
* Converts image proxy in YUV to RGB.
*
* Currently this config supports the devices which generated NV21, NV12, I420 YUV layout,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
index 947ac42f..a43a5e2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -201,7 +201,7 @@
}
} else if (imageFormat == ImageFormat.YUV_420_888) {
return ImageUtil.yuvImageToJpegByteArray(image, shouldCropImage ? image.getCropRect() :
- null, jpegQuality);
+ null, jpegQuality, 0 /* rotationDegrees */);
} else {
Logger.w(TAG, "Unrecognized image format: " + imageFormat);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
deleted file mode 100644
index 4d9250b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.Matrix;
-import android.media.ImageReader;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.TagBundle;
-
-/**
- * An ImageReaderProxy implementation that allows to modify the ImageInfo data of the images
- * retrieved.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class ModifiableImageReaderProxy extends AndroidImageReaderProxy {
- private volatile TagBundle mTagBundle = null;
- private volatile Long mTimestamp = null;
- private volatile Integer mRotationDegrees = null;
- private volatile Matrix mSensorToBufferTransformMatrix = null;
-
- ModifiableImageReaderProxy(@NonNull ImageReader imageReader) {
- super(imageReader);
- }
-
- void setImageTagBundle(@NonNull TagBundle tagBundle) {
- mTagBundle = tagBundle;
- }
-
- void setImageTimeStamp(long timestamp) {
- mTimestamp = timestamp;
- }
-
- void setImageRotationDegrees(int rotationDegrees) {
- mRotationDegrees = rotationDegrees;
- }
-
- void setImageSensorToBufferTransformaMatrix(@NonNull Matrix matrix) {
- mSensorToBufferTransformMatrix = matrix;
- }
-
- @Nullable
- @Override
- public ImageProxy acquireLatestImage() {
- return modifyImage(super.acquireNextImage());
- }
-
- @Nullable
- @Override
- public ImageProxy acquireNextImage() {
- return modifyImage(super.acquireNextImage());
- }
-
- private ImageProxy modifyImage(ImageProxy imageProxy) {
- ImageInfo origin = imageProxy.getImageInfo();
- ImageInfo imageInfo = ImmutableImageInfo.create(
- mTagBundle != null ? mTagBundle : origin.getTagBundle(),
- mTimestamp != null ? mTimestamp.longValue() : origin.getTimestamp(),
- mRotationDegrees != null ? mRotationDegrees.intValue() :
- origin.getRotationDegrees(),
- mSensorToBufferTransformMatrix != null ? mSensorToBufferTransformMatrix :
- origin.getSensorToBufferTransformMatrix());
- return new SettableImageProxy(imageProxy, imageInfo);
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 585948a..36096c8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -19,15 +19,12 @@
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
-import static androidx.camera.core.impl.PreviewConfig.IMAGE_INFO_PROCESSOR;
import static androidx.camera.core.impl.PreviewConfig.OPTION_BACKGROUND_EXECUTOR;
import static androidx.camera.core.impl.PreviewConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_RESOLUTION;
import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.PreviewConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_RGBA8888_SURFACE_REQUIRED;
import static androidx.camera.core.impl.PreviewConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.PreviewConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.impl.PreviewConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
@@ -49,8 +46,6 @@
import android.graphics.SurfaceTexture;
import android.media.ImageReader;
import android.media.MediaCodec;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.util.Pair;
import android.util.Size;
import android.view.Display;
@@ -66,18 +61,13 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageFormatConstants;
-import androidx.camera.core.impl.ImageInfoProcessor;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
@@ -88,7 +78,6 @@
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.CameraCaptureResultImageInfo;
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.core.internal.TargetConfig;
import androidx.camera.core.internal.ThreadConfig;
@@ -222,18 +211,15 @@
Threads.checkMainThread();
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
- final CaptureProcessor captureProcessor = config.getCaptureProcessor(null);
// Close previous session's deferrable surface before creating new one
clearPipeline();
- boolean isRGBA8888SurfaceRequired = config.isRgba8888SurfaceRequired(false);
-
// TODO: Can be improved by only restarting part of the pipeline when using the
// CaptureProcessor. e.g. only update the output Surface (between Processor/App), and
// still use the same input Surface (between Camera/Processor). It's just simpler for now.
final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
- isRGBA8888SurfaceRequired, this::notifyReset);
+ /* isRGBA8888Required */ false, this::notifyReset);
mCurrentSurfaceRequest = surfaceRequest;
if (mSurfaceProvider != null) {
@@ -241,54 +227,7 @@
sendSurfaceRequest();
}
- if (captureProcessor != null) {
- CaptureStage captureStage = new CaptureStage.DefaultCaptureStage();
- // TODO: To allow user to use an Executor for the processing.
- HandlerThread handlerThread = new HandlerThread(
- CameraXThreads.TAG + "preview_processing");
- handlerThread.start();
-
- String tagBundleKey = Integer.toString(captureStage.hashCode());
-
- ProcessingSurface processingSurface = new ProcessingSurface(
- resolution.getWidth(),
- resolution.getHeight(),
- config.getInputFormat(),
- new Handler(handlerThread.getLooper()),
- captureStage,
- captureProcessor,
- surfaceRequest.getDeferrableSurface(),
- tagBundleKey);
-
- sessionConfigBuilder.addCameraCaptureCallback(
- processingSurface.getCameraCaptureCallback());
-
- processingSurface.getTerminationFuture().addListener(handlerThread::quitSafely,
- CameraXExecutors.directExecutor());
-
- mSessionDeferrableSurface = processingSurface;
-
- // Use CaptureStage object as the key for TagBundle
- sessionConfigBuilder.addTag(tagBundleKey, captureStage.getId());
- } else {
- final ImageInfoProcessor processor = config.getImageInfoProcessor(null);
-
- if (processor != null) {
- sessionConfigBuilder.addCameraCaptureCallback(new CameraCaptureCallback() {
- @Override
- public void onCaptureCompleted(
- @NonNull CameraCaptureResult cameraCaptureResult) {
- super.onCaptureCompleted(cameraCaptureResult);
- if (processor.process(
- new CameraCaptureResultImageInfo(cameraCaptureResult))) {
- notifyUpdated();
- }
- }
- });
- }
- mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
- }
-
+ mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, resolution);
return sessionConfigBuilder;
}
@@ -631,13 +570,8 @@
@Override
protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
- if (builder.getMutableConfig().retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, null)
- != null) {
- builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
- } else {
- builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
- ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
- }
+ builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
// Merges Preview's default max resolution setting when resolution selector is used
ResolutionSelector resolutionSelector =
@@ -1207,41 +1141,6 @@
return this;
}
- /**
- * Sets if the surface requires RGBA8888 format.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setIsRgba8888SurfaceRequired(boolean isRgba8888SurfaceRequired) {
- getMutableConfig().insertOption(
- OPTION_RGBA8888_SURFACE_REQUIRED, isRgba8888SurfaceRequired);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setImageInfoProcessor(@NonNull ImageInfoProcessor processor) {
- getMutableConfig().insertOption(IMAGE_INFO_PROCESSOR, processor);
- return this;
- }
-
- /**
- * Sets the {@link CaptureProcessor}.
- *
- * @param captureProcessor The requested capture processor for extension.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
- getMutableConfig().insertOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, captureProcessor);
- return this;
- }
-
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java
new file mode 100644
index 0000000..139162e
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/**
+ * Exception throw when effects post-processing fails.
+ *
+ * <p>Implementation of {@link SurfaceProcessor} throws this exception from
+ * {@link SurfaceProcessor#onInputSurface} or {@link SurfaceProcessor#onOutputSurface} when an
+ * error occurs during effect processing.
+ *
+ * @see SurfaceProcessor
+ */
+public class ProcessingException extends Exception {
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
deleted file mode 100644
index 23148dc..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingSurface.java
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-
-import android.graphics.PixelFormat;
-import android.graphics.SurfaceTexture;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.ImageReaderProxy;
-import androidx.camera.core.impl.SingleImageProxyBundle;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.FutureChain;
-import androidx.camera.core.impl.utils.futures.Futures;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.Executor;
-
-/**
- * A {@link DeferrableSurface} that does processing and outputs a {@link SurfaceTexture}.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-final class ProcessingSurface extends DeferrableSurface {
- private static final String TAG = "ProcessingSurfaceTextur";
-
- // Synthetic Accessor
- @SuppressWarnings("WeakerAccess")
- final Object mLock = new Object();
-
- // Callback when Image is ready from InputImageReader.
- private final ImageReaderProxy.OnImageAvailableListener mTransformedListener =
- reader -> {
- synchronized (mLock) {
- imageIncoming(reader);
- }
- };
-
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
- boolean mReleased = false;
-
- @NonNull
- private final Size mResolution;
-
- private final MetadataImageReader mInputImageReader;
-
- // The Surface that is backed by mInputImageReader
- private final Surface mInputSurface;
-
- private final Handler mImageReaderHandler;
-
- // Maximum number of images in the input ImageReader
- private static final int MAX_IMAGES = 2;
-
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- final CaptureStage mCaptureStage;
-
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @NonNull
- @GuardedBy("mLock")
- final CaptureProcessor mCaptureProcessor;
-
- private final CameraCaptureCallback mCameraCaptureCallback;
- private final DeferrableSurface mOutputDeferrableSurface;
-
- private String mTagBundleKey;
- /**
- * Create a {@link ProcessingSurface} with specific configurations.
- * @param width Width of the ImageReader
- * @param height Height of the ImageReader
- * @param format Image format
- * @param handler Handler for executing
- * {@link ImageReaderProxy.OnImageAvailableListener}. If
- * this is
- * {@code null} then execution will be done on the calling
- * thread's
- * {@link Looper}.
- * @param captureStage The {@link CaptureStage} includes the processing information
- * @param captureProcessor The {@link CaptureProcessor} to be invoked when the
- * Images are ready
- * @param outputSurface The {@link DeferrableSurface} used as the output of
- * processing.
- * @param tagBundleKey The key for tagBundle to get correct image. Usually the key comes
- * from the CaptureStage's hash code.
- */
- ProcessingSurface(int width, int height, int format, @Nullable Handler handler,
- @NonNull CaptureStage captureStage, @NonNull CaptureProcessor captureProcessor,
- @NonNull DeferrableSurface outputSurface, @NonNull String tagBundleKey) {
- super(new Size(width, height), format);
- mResolution = new Size(width, height);
-
- if (handler != null) {
- mImageReaderHandler = handler;
- } else {
- Looper looper = Looper.myLooper();
-
- if (looper == null) {
- throw new IllegalStateException(
- "Creating a ProcessingSurface requires a non-null Handler, or be created "
- + " on a thread with a Looper.");
- }
-
- mImageReaderHandler = new Handler(looper);
- }
-
- Executor executor = CameraXExecutors.newHandlerExecutor(mImageReaderHandler);
-
- // input
- mInputImageReader = new MetadataImageReader(
- width,
- height,
- format,
- MAX_IMAGES);
- mInputImageReader.setOnImageAvailableListener(mTransformedListener, executor);
- mInputSurface = mInputImageReader.getSurface();
- mCameraCaptureCallback = mInputImageReader.getCameraCaptureCallback();
-
- // processing
- mCaptureProcessor = captureProcessor;
- mCaptureProcessor.onResolutionUpdate(mResolution);
- mCaptureStage = captureStage;
-
- // output
- mOutputDeferrableSurface = outputSurface;
-
- mTagBundleKey = tagBundleKey;
-
- Futures.addCallback(outputSurface.getSurface(),
- new FutureCallback<Surface>() {
- @Override
- public void onSuccess(@Nullable Surface surface) {
- synchronized (mLock) {
- mCaptureProcessor.onOutputSurface(surface, PixelFormat.RGBA_8888);
- }
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- Logger.e(TAG, "Failed to extract Listenable<Surface>.", t);
- }
- }, directExecutor());
-
- getTerminationFuture().addListener(this::release, directExecutor());
- }
-
- @Override
- @NonNull
- public ListenableFuture<Surface> provideSurface() {
- // Before returning input surface to configure the capture session, ensures
- // output surface is ready because output surface will be needed just before
- // configuring capture session.
- return FutureChain.from(mOutputDeferrableSurface.getSurface())
- .transform(outputSurface -> mInputSurface, CameraXExecutors.directExecutor());
- }
-
- /**
- * Returns necessary camera callbacks to retrieve metadata from camera result.
- *
- * @throws IllegalStateException if {@link #release()} has already been called
- */
- @Nullable
- CameraCaptureCallback getCameraCaptureCallback() {
- synchronized (mLock) {
- if (mReleased) {
- throw new IllegalStateException("ProcessingSurface already released!");
- }
-
- return mCameraCaptureCallback;
- }
- }
-
- /**
- * Close the {@link ProcessingSurface}.
- *
- * <p> After closing the ProcessingSurface it should not be used again. A new instance
- * should be created.
- *
- * <p>This should only be called once the ProcessingSurface has been terminated, i.e., it's
- * termination future retrieved via {@link #getTerminationFuture()}} has completed.
- */
- private void release() {
- synchronized (mLock) {
- if (mReleased) {
- return;
- }
-
- mInputImageReader.clearOnImageAvailableListener();
-
- // Since the ProcessingSurface DeferrableSurface has been terminated, it is safe to
- // close the inputs.
- mInputImageReader.close();
- mInputSurface.release();
-
- // Now that the inputs are closed, we can close the output surface.
- mOutputDeferrableSurface.close();
-
- mReleased = true;
- }
- }
-
- // Incoming Image from InputImageReader. Acquires it and add to SettableImageProxyBundle.
- @SuppressWarnings("WeakerAccess")
- @GuardedBy("mLock")
- void imageIncoming(ImageReaderProxy imageReader) {
- if (mReleased) {
- return;
- }
-
- ImageProxy image = null;
- try {
- image = imageReader.acquireNextImage();
- } catch (IllegalStateException e) {
- Logger.e(TAG, "Failed to acquire next image.", e);
- }
-
- if (image == null) {
- return;
- }
-
- ImageInfo imageInfo = image.getImageInfo();
- if (imageInfo == null) {
- image.close();
- return;
- }
-
- Integer tagValue = (Integer) imageInfo.getTagBundle().getTag(mTagBundleKey);
- if (tagValue == null) {
- image.close();
- return;
- }
-
- if (mCaptureStage.getId() != tagValue) {
- Logger.w(TAG, "ImageProxyBundle does not contain this id: " + tagValue);
- image.close();
- } else {
- SingleImageProxyBundle imageProxyBundle = new SingleImageProxyBundle(image,
- mTagBundleKey);
- try {
- // Increments the use count to prevent the surface from being closed during the
- // processing duration.
- incrementUseCount();
- } catch (SurfaceClosedException e) {
- Logger.d(TAG, "The ProcessingSurface has been closed. Don't process the incoming "
- + "image.");
- imageProxyBundle.close();
- return;
- }
- mCaptureProcessor.process(imageProxyBundle);
- imageProxyBundle.close();
- // Decrements the use count to allow the surface to be closed.
- decrementUseCount();
- }
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index 1ee0da0..3bde336 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -37,10 +37,8 @@
* <p>Contains a {@link Surface} and its characteristics along with methods to manage the
* lifecycle of the {@link Surface}.
*
- * @hide
* @see SurfaceProcessor#onOutputSurface(SurfaceOutput)
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface SurfaceOutput {
/**
@@ -62,13 +60,7 @@
/**
* This field indicates that what purpose the {@link Surface} will be used for.
*
- * <ul>
- * <li>{@link SurfaceProcessor#PREVIEW} if the {@link Surface} will be used for {@link Preview}.
- * <li>{@link SurfaceProcessor#VIDEO_CAPTURE} if the {@link Surface} will be used for video
- * capture.
- * <li>{@link SurfaceProcessor#PREVIEW} | {@link SurfaceProcessor#VIDEO_CAPTURE} if the output
- * {@link Surface} will be used for sharing a single stream for both preview and video capture.
- * </ul>
+ * <p>{@link CameraEffect#PREVIEW} if the {@link Surface} will be used for {@link Preview}.
*/
@CameraEffect.Targets
int getTargets();
@@ -81,37 +73,60 @@
/**
* Gets the format of the {@link Surface}.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
int getFormat();
/**
* Get the rotation degrees.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
int getRotationDegrees();
/**
* Call this method to mark the {@link Surface} as no longer in use.
*
- * <p>After this is called, the implementation should stop writing to the {@link Surface}
- * provided via {@link #getSurface}
+ * <p>Once the {@link SurfaceProcessor} implementation receives a request to close the
+ * {@link Surface}, it should call this method to acknowledge after stop writing to the
+ * {@link Surface}. Writing to the {@link Surface} after calling this method might cause
+ * errors.
*/
void close();
/**
- * Updates the 4 x 4 transformation matrix retrieved from {@link SurfaceTexture
- * #getTransformMatrix}.
+ * Updates the 4 x 4 transformation matrix retrieved from the input {@link Surface}.
*
- * <p>This method applies an additional transformation on top of the value of
- * {@link SurfaceTexture#getTransformMatrix}. The result is matrix of the same format, which
- * is a transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1)
- * with s and t in the inclusive range [0, 1] to the texture coordinate that should be used
- * to sample that location from the texture. The matrix is stored in column-major order so that
- * it may be passed directly to OpenGL ES via the {@code glLoadMatrixf} or {@code
- * glUniformMatrix4fv} functions.
+ * <p>When the input {@link Surface} of {@link SurfaceProcessor} is backed by a
+ * {@link SurfaceTexture}, use this method to update the texture transform matrix.
+ *
+ * <p>After retrieving the transform matrix from {@link SurfaceTexture#getTransformMatrix},
+ * the {@link SurfaceProcessor} implementation should always call this method to update the
+ * value. The result is a matrix of the same format, which is a transform matrix maps 2D
+ * homogeneous texture coordinates of the form (s, t, 0, 1) with s and t in the inclusive
+ * range [0, 1] to the texture coordinate that should be used to sample that location from
+ * the texture. The matrix is stored in column-major order so that it may be passed directly
+ * to OpenGL ES via the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions.
*
* <p>The additional transformation is calculated based on the target rotation, target
* resolution and the {@link ViewPort} associated with the target {@link UseCase}. The value
- * could also include workarounds for device specific quirks.
+ * could also include workarounds for device specific bugs. For example, correcting a
+ * stretched camera output stream.
+ *
+ * <p>Code sample:
+ * <pre><code>
+ * float[] transform = new float[16];
+ * float[] updatedTransform = new float[16];
+ *
+ * surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
+ * surfaceTexture.getTransformMatrix(transform);
+ * outputSurface.updateTransformMatrix(updatedTransform, transform);
+ * // Use the value of updatedTransform for OpenGL rendering.
+ * });
+ * </code></pre>
*
* @param updated the array into which the 4x4 matrix will be stored. The array must
* have exactly 16 elements.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
index 56e6aec..a622897 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
@@ -20,20 +20,58 @@
import android.view.Surface;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import androidx.core.util.Consumer;
/**
* Interface to implement a GPU-based post-processing effect.
*
- * <p>This interface is for implementing a GPU effect for the {@link Preview} and/or
- * {@code VideoCapture} {@link UseCase}. Both the input and the output of the implementation
- * are {@link Surface}s. It's recommended to use graphics API such as OpenGL or Vulkan to access
- * the {@link Surface}.
+ * <p>This interface is for implementing a GPU effect for the {@link Preview} {@link UseCase}.
+ * Both the input and the output of the implementation are {@link Surface}s. It's recommended to
+ * use graphics API such as OpenGL or Vulkan to access the {@link Surface}.
*
- * @hide
+ * <p>If the implementation fails to process frames, for example, fails to allocate
+ * the resources, it should throw a {@link ProcessingException} in either {@link #onInputSurface} or
+ * {@link #onOutputSurface} to notify CameraX. If the implementation encounters an error after the
+ * pipeline is running, it should invalidate the input {@link Surface} by calling
+ * {@link SurfaceRequest#invalidate()}, then throwing a {@link ProcessingException} when
+ * {@link SurfaceProcessor#onInputSurface} is invoked again.
+ *
+ * <p>Once the implementation throws an exception, CameraX will treat it as an unrecoverable error
+ * and abort the pipeline. If the {@link SurfaceOutput#getTargets()} is
+ * {@link CameraEffect#PREVIEW}, CameraX will not propagate the error to the app. It's the
+ * implementation's responsibility to notify the app. For example:
+ *
+ * <pre><code>
+ * class SurfaceProcessorImpl implements SurfaceProcessor {
+ *
+ * Consumer<Exception> mErrorListener;
+ *
+ * SurfaceProcessorImpl(@NonNull Consumer<Exception> errorListener) {
+ * mErrorListener = errorListener;
+ * }
+ *
+ * void onInputSurface(@NonNull SurfaceRequest request) throws ProcessingException {
+ * try {
+ * // Setup the input stream.
+ * } catch (Exception e) {
+ * // Notify the app before throwing a ProcessingException.
+ * mErrorListener.accept(e)
+ * throw new ProcessingException(e);
+ * }
+ * }
+ *
+ * void onOutputSurface(@NonNull SurfaceRequest request) throws ProcessingException {
+ * try {
+ * // Setup the output streams.
+ * } catch (Exception e) {
+ * // Notify the app before throwing a ProcessingException.
+ * mErrorListener.accept(e)
+ * throw new ProcessingException(e);
+ * }
+ * }
+ * }
+ * </code></pre>
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface SurfaceProcessor {
/**
@@ -44,15 +82,44 @@
* listen for the {@link SurfaceTexture#setOnFrameAvailableListener} to get the incoming
* frames.
*
+ * <p>If the implementation encounters errors in creating the input {@link Surface}, it
+ * should throw an {@link ProcessingException} to notify CameraX.
+ *
+ * <p>The implementation can replace a previously provided {@link Surface} by invoking
+ * {@link SurfaceRequest#invalidate()}. Once invoked, CameraX will restart the camera
+ * pipeline and call {@link #onInputSurface} again with another {@link SurfaceRequest}.
+ *
* <p>The value of the {@link SurfaceTexture#getTransformMatrix} will need an additional
* transformation. CameraX calculates the additional transformation based on {@link UseCase}
* configurations such as {@link ViewPort} and target rotation, and provide the value via
* {@link SurfaceOutput#updateTransformMatrix(float[], float[])}.
*
+ * Code sample:
+ * <pre><code>
+ * // Create Surface based on the request.
+ * SurfaceTexture surfaceTexture = SurfaceTexture(textureName);
+ * surfaceTexture.setDefaultBufferSize(request.resolution.width, request.resolution.height);
+ * Surface surface = Surface(surfaceTexture);
+ *
+ * // Provide the Surface to CameraX, and cleanup when it's no longer used.
+ * surfaceRequest.provideSurface(surface, executor, result -> {
+ * surfaceTexture.setOnFrameAvailableListener(null)
+ * surfaceTexture.release()
+ * surface.release()
+ * });
+ *
+ * // Listen to the incoming frames.
+ * surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
+ * // Process the incoming frames and draw to the output Surface from #onOutputSurface
+ * }, handler);
+ * </code></pre>
+ *
* @param request a request to provide {@link Surface} for input.
+ * @throws ProcessingException if the implementation fails to fulfill the
+ * {@link SurfaceRequest}.
* @see SurfaceRequest
*/
- void onInputSurface(@NonNull SurfaceRequest request);
+ void onInputSurface(@NonNull SurfaceRequest request) throws ProcessingException;
/**
* Invoked when CameraX provides output Surface(s) for drawing processed frames.
@@ -63,12 +130,17 @@
* {@link Surface}. Then, the implementation should call {@link SurfaceOutput#close()} after it
* stops drawing to the {@link Surface}.
*
- * <p> When drawing to the {@link Surface}, the implementation should apply an additional
+ * <p>If the implementation encounters an error and cannot consume the {@link Surface},
+ * it should throw an {@link ProcessingException} to notify CameraX.
+ *
+ * <p>When drawing to the {@link Surface}, the implementation should apply an additional
* transformation to the input {@link Surface} by calling
* {@link SurfaceOutput#updateTransformMatrix(float[], float[])} with the value of
* {@link SurfaceTexture#getTransformMatrix(float[])}} from the input {@link Surface}.
*
* @param surfaceOutput contains a {@link Surface} for drawing processed frames.
+ * @throws ProcessingException if the implementation fails to consume the {@link SurfaceOutput}.
+ * @see SurfaceOutput
*/
- void onOutputSurface(@NonNull SurfaceOutput surfaceOutput);
+ void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) throws ProcessingException;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
index 5008b5c..e621821 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -26,7 +26,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
import androidx.lifecycle.Lifecycle;
import java.util.ArrayList;
@@ -78,10 +77,7 @@
/**
* Gets the {@link CameraEffect}s.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public List<CameraEffect> getEffects() {
return mEffects;
@@ -127,10 +123,7 @@
*
* <p>Once added, CameraX will use the {@link CameraEffect}s to process the outputs of
* the {@link UseCase}s.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public Builder addEffect(@NonNull CameraEffect cameraEffect) {
mEffects.add(cameraEffect);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
deleted file mode 100644
index bd6734b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ /dev/null
@@ -1,2112 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_BIT_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_CHANNEL_COUNT;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_MIN_BUFFER_SIZE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_AUDIO_SAMPLE_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_BIT_RATE;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_INTRA_FRAME_INTERVAL;
-import static androidx.camera.core.impl.VideoCaptureConfig.OPTION_VIDEO_FRAME_RATE;
-import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
-import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
-import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
-import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
-
-import android.Manifest;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.location.Location;
-import android.media.AudioFormat;
-import android.media.AudioRecord;
-import android.media.CamcorderProfile;
-import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecInfo.CodecCapabilities;
-import android.media.MediaFormat;
-import android.media.MediaMuxer;
-import android.media.MediaRecorder.AudioSource;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-import android.util.Pair;
-import android.util.Size;
-import android.view.Display;
-import android.view.Surface;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.GuardedBy;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RequiresPermission;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.UiThread;
-import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.ConfigProvider;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
-import androidx.camera.core.impl.ImmediateSurface;
-import androidx.camera.core.impl.MutableConfig;
-import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.OptionsBundle;
-import androidx.camera.core.impl.SessionConfig;
-import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.impl.VideoCaptureConfig;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.ThreadConfig;
-import androidx.camera.core.internal.utils.VideoUtil;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A use case for taking a video.
- *
- * <p>This class is designed for simple video capturing. It gives basic configuration of the
- * recorded video such as resolution and file format.
- *
- * @deprecated Use {@link androidx.camera.video.VideoCapture} instead.
- *
- * @hide
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@Deprecated
-@RestrictTo(Scope.LIBRARY_GROUP)
-public final class VideoCapture extends UseCase {
-
- ////////////////////////////////////////////////////////////////////////////////////////////
- // [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
- // they could be created in the constructor.
- ////////////////////////////////////////////////////////////////////////////////////////////
-
- /**
- * An unknown error occurred.
- *
- * <p>See message parameter in onError callback or log for more details.
- */
- public static final int ERROR_UNKNOWN = 0;
- /**
- * An error occurred with encoder state, either when trying to change state or when an
- * unexpected state change occurred.
- */
- public static final int ERROR_ENCODER = 1;
- /** An error with muxer state such as during creation or when stopping. */
- public static final int ERROR_MUXER = 2;
- /**
- * An error indicating start recording was called when video recording is still in progress.
- */
- public static final int ERROR_RECORDING_IN_PROGRESS = 3;
- /**
- * An error indicating the file saving operations.
- */
- public static final int ERROR_FILE_IO = 4;
- /**
- * An error indicating this VideoCapture is not bound to a camera.
- */
- public static final int ERROR_INVALID_CAMERA = 5;
- /**
- * An error indicating the video file is too short.
- * <p> The output file will be deleted if the OutputFileOptions is backed by File or uri.
- */
- public static final int ERROR_RECORDING_TOO_SHORT = 6;
-
- /**
- * Provides a static configuration with implementation-agnostic options.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- public static final Defaults DEFAULT_CONFIG = new Defaults();
- private static final String TAG = "VideoCapture";
- /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
- private static final int DEQUE_TIMEOUT_USEC = 10000;
- /** Android preferred mime type for AVC video. */
- private static final String VIDEO_MIME_TYPE = "video/avc";
- private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
- /** Camcorder profiles quality list */
- private static final int[] CamcorderQuality = {
- CamcorderProfile.QUALITY_2160P,
- CamcorderProfile.QUALITY_1080P,
- CamcorderProfile.QUALITY_720P,
- CamcorderProfile.QUALITY_480P
- };
-
- private final BufferInfo mVideoBufferInfo = new BufferInfo();
- private final Object mMuxerLock = new Object();
- private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
- private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
- private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
- private final BufferInfo mAudioBufferInfo = new BufferInfo();
- /** For record the first sample written time. */
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- public final AtomicBoolean mIsFirstVideoKeyFrameWrite = new AtomicBoolean(false);
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- public final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
-
- ////////////////////////////////////////////////////////////////////////////////////////////
- // [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
- ////////////////////////////////////////////////////////////////////////////////////////////
-
- /** Thread on which all encoding occurs. */
- private HandlerThread mVideoHandlerThread;
- private Handler mVideoHandler;
- /** Thread on which audio encoding occurs. */
- private HandlerThread mAudioHandlerThread;
- private Handler mAudioHandler;
-
- @NonNull
- MediaCodec mVideoEncoder;
- @NonNull
- private MediaCodec mAudioEncoder;
- @Nullable
- private ListenableFuture<Void> mRecordingFuture = null;
- @NonNull
- private SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
-
- ////////////////////////////////////////////////////////////////////////////////////////////
- // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
- ////////////////////////////////////////////////////////////////////////////////////////////
-
- /** The muxer that writes the encoding data to file. */
- @GuardedBy("mMuxerLock")
- private MediaMuxer mMuxer;
- private final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
- /** The index of the video track used by the muxer. */
- @GuardedBy("mMuxerLock")
- private int mVideoTrackIndex;
- /** The index of the audio track used by the muxer. */
- @GuardedBy("mMuxerLock")
- private int mAudioTrackIndex;
- /** Surface the camera writes to, which the videoEncoder uses as input. */
- Surface mCameraSurface;
-
- /** audio raw data */
- @Nullable
- private volatile AudioRecord mAudioRecorder;
- private volatile int mAudioBufferSize;
- private volatile boolean mIsRecording = false;
- private int mAudioChannelCount;
- private int mAudioSampleRate;
- private int mAudioBitRate;
- private DeferrableSurface mDeferrableSurface;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- volatile Uri mSavedVideoUri;
- private volatile ParcelFileDescriptor mParcelFileDescriptor;
- private final AtomicBoolean mIsAudioEnabled = new AtomicBoolean(true);
-
- private VideoEncoderInitStatus mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED;
- @Nullable
- private Throwable mVideoEncoderErrorMessage;
-
- /**
- * Creates a new video capture use case from the given configuration.
- *
- * @param config for this use case instance
- */
- VideoCapture(@NonNull VideoCaptureConfig config) {
- super(config);
- }
-
- /** Creates a {@link MediaFormat} using parameters from the configuration */
- private static MediaFormat createVideoMediaFormat(VideoCaptureConfig config, Size resolution) {
- MediaFormat format =
- MediaFormat.createVideoFormat(
- VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
- format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
- format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
- format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
- format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
-
- return format;
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @Nullable
- public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
- @NonNull UseCaseConfigFactory factory) {
- Config captureConfig = factory.getConfig(
- UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
- ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY);
-
- if (applyDefaultConfig) {
- captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
- }
-
- return captureConfig == null ? null :
- getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @SuppressWarnings("WrongConstant")
- @Override
- @RestrictTo(Scope.LIBRARY_GROUP)
- public void onAttached() {
- mVideoHandlerThread = new HandlerThread(CameraXThreads.TAG + "video encoding thread");
- mAudioHandlerThread = new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
-
- // video thread start
- mVideoHandlerThread.start();
- mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
-
- // audio thread start
- mAudioHandlerThread.start();
- mAudioHandler = new Handler(mAudioHandlerThread.getLooper());
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @Override
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
- if (mCameraSurface != null) {
- mVideoEncoder.stop();
- mVideoEncoder.release();
- mAudioEncoder.stop();
- mAudioEncoder.release();
- releaseCameraSurface(false);
- }
-
- try {
- mVideoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
- mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
- } catch (IOException e) {
- throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
- }
-
- setupEncoder(getCameraId(), suggestedResolution);
- // VideoCapture has to be active to apply SessionConfig's template type.
- notifyActive();
- return suggestedResolution;
- }
-
- /**
- * Starts recording video, which continues until {@link VideoCapture#stopRecording()} is
- * called.
- *
- * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * @param outputFileOptions Location to save the video capture
- * @param executor The executor in which the callback methods will be run.
- * @param callback Callback for when the recorded video saving completion or failure.
- */
- @SuppressWarnings("ObjectToString")
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- public void startRecording(
- @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
- @NonNull OnVideoSavedCallback callback) {
- if (Looper.getMainLooper() != Looper.myLooper()) {
- CameraXExecutors.mainThreadExecutor().execute(() -> startRecording(outputFileOptions,
- executor, callback));
- return;
- }
- Logger.i(TAG, "startRecording");
- mIsFirstVideoKeyFrameWrite.set(false);
- mIsFirstAudioSampleWrite.set(false);
-
- OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
-
- CameraInternal attachedCamera = getCamera();
- if (attachedCamera == null) {
- // Not bound. Notify callback.
- postListener.onError(ERROR_INVALID_CAMERA,
- "Not bound to a Camera [" + VideoCapture.this + "]", null);
- return;
- }
-
- // Check video encoder initialization status, if there is any error happened
- // return error callback directly.
- if (mVideoEncoderInitStatus
- == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE
- || mVideoEncoderInitStatus
- == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED
- || mVideoEncoderInitStatus
- == VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED) {
- postListener.onError(ERROR_ENCODER, "Video encoder initialization failed before start"
- + " recording ", mVideoEncoderErrorMessage);
- return;
- }
-
- if (!mEndOfAudioVideoSignal.get()) {
- postListener.onError(
- ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
- null);
- return;
- }
-
- if (mIsAudioEnabled.get()) {
- try {
- // Audio input start
- if (mAudioRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
- mAudioRecorder.startRecording();
- }
- } catch (IllegalStateException e) {
- // Disable the audio if the audio input cannot start. And Continue the recording
- // without audio.
- Logger.i(TAG,
- "AudioRecorder cannot start recording, disable audio." + e.getMessage());
- mIsAudioEnabled.set(false);
- releaseAudioInputResource();
- }
-
- // Gets the AudioRecorder's state
- if (mAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
- Logger.i(TAG,
- "AudioRecorder startRecording failed - incorrect state: "
- + mAudioRecorder.getRecordingState());
- mIsAudioEnabled.set(false);
- releaseAudioInputResource();
- }
- }
-
- AtomicReference<Completer<Void>> recordingCompleterRef = new AtomicReference<>();
- mRecordingFuture = CallbackToFutureAdapter.getFuture(
- completer -> {
- recordingCompleterRef.set(completer);
- return "startRecording";
- });
- Completer<Void> recordingCompleter =
- Preconditions.checkNotNull(recordingCompleterRef.get());
-
- mRecordingFuture.addListener(() -> {
- mRecordingFuture = null;
- // Do the setup of the videoEncoder at the end of video recording instead of at the
- // start of recording because it requires attaching a new Surface. This causes a
- // glitch so we don't want that to incur latency at the start of capture.
- if (getCamera() != null) {
- // Ensure the use case is bound. Asynchronous stopping procedure may occur after
- // the use case is unbound, i.e. after onDetached().
- setupEncoder(getCameraId(), getAttachedSurfaceResolution());
- notifyReset();
- }
- }, CameraXExecutors.mainThreadExecutor());
-
- try {
- // video encoder start
- Logger.i(TAG, "videoEncoder start");
- mVideoEncoder.start();
-
- // audio encoder start
- if (mIsAudioEnabled.get()) {
- Logger.i(TAG, "audioEncoder start");
- mAudioEncoder.start();
- }
- } catch (IllegalStateException e) {
- recordingCompleter.set(null);
- postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail", e);
- return;
- }
-
- try {
- synchronized (mMuxerLock) {
- mMuxer = initMediaMuxer(outputFileOptions);
- Preconditions.checkNotNull(mMuxer);
- mMuxer.setOrientationHint(getRelativeRotation(attachedCamera));
-
- Metadata metadata = outputFileOptions.getMetadata();
- if (metadata != null && metadata.location != null) {
- mMuxer.setLocation(
- (float) metadata.location.getLatitude(),
- (float) metadata.location.getLongitude());
- }
- }
- } catch (IOException e) {
- recordingCompleter.set(null);
- postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
- return;
- }
-
- mEndOfVideoStreamSignal.set(false);
- mEndOfAudioStreamSignal.set(false);
- mEndOfAudioVideoSignal.set(false);
- mIsRecording = true;
-
- // Attach Surface to repeating request.
- mSessionConfigBuilder.clearSurfaces();
- mSessionConfigBuilder.addSurface(mDeferrableSurface);
- updateSessionConfig(mSessionConfigBuilder.build());
- notifyUpdated();
-
- if (mIsAudioEnabled.get()) {
- mAudioHandler.post(() -> audioEncode(postListener));
- }
-
- String cameraId = getCameraId();
- Size resolution = getAttachedSurfaceResolution();
- mVideoHandler.post(
- () -> {
- boolean errorOccurred = videoEncode(postListener, cameraId, resolution,
- outputFileOptions);
- if (!errorOccurred) {
- postListener.onVideoSaved(new OutputFileResults(mSavedVideoUri));
- mSavedVideoUri = null;
- }
- recordingCompleter.set(null);
- });
- }
-
- /**
- * Stops recording video, this must be called after {@link
- * VideoCapture#startRecording(OutputFileOptions, Executor, OnVideoSavedCallback)} is
- * called.
- *
- * <p>stopRecording() is asynchronous API. User need to check if {@link
- * OnVideoSavedCallback#onVideoSaved(OutputFileResults)} or
- * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
- * before startRecording.
- */
- public void stopRecording() {
- if (Looper.getMainLooper() != Looper.myLooper()) {
- CameraXExecutors.mainThreadExecutor().execute(() -> stopRecording());
- return;
- }
- Logger.i(TAG, "stopRecording");
-
- mSessionConfigBuilder.clearSurfaces();
- mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
- updateSessionConfig(mSessionConfigBuilder.build());
- notifyUpdated();
-
- if (mIsRecording) {
- if (mIsAudioEnabled.get()) {
- // Stop audio encoder thread, and wait video encoder and muxer stop.
- mEndOfAudioStreamSignal.set(true);
- } else {
- // Audio is disabled, stop video encoder thread directly.
- mEndOfVideoStreamSignal.set(true);
- }
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- public void onDetached() {
- stopRecording();
-
- if (mRecordingFuture != null) {
- mRecordingFuture.addListener(() -> releaseResources(),
- CameraXExecutors.mainThreadExecutor());
- } else {
- releaseResources();
- }
- }
-
- private void releaseResources() {
- mVideoHandlerThread.quitSafely();
-
- // audio encoder release
- releaseAudioInputResource();
-
- if (mCameraSurface != null) {
- releaseCameraSurface(true);
- }
- }
-
- private void releaseAudioInputResource() {
- mAudioHandlerThread.quitSafely();
- if (mAudioEncoder != null) {
- mAudioEncoder.release();
- mAudioEncoder = null;
- }
-
- if (mAudioRecorder != null) {
- mAudioRecorder.release();
- mAudioRecorder = null;
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @NonNull
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
- return Builder.fromConfig(config);
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @UiThread
- @Override
- public void onStateDetached() {
- stopRecording();
- }
-
- @UiThread
- private void releaseCameraSurface(final boolean releaseVideoEncoder) {
- if (mDeferrableSurface == null) {
- return;
- }
-
- final MediaCodec videoEncoder = mVideoEncoder;
-
- // Calling close should allow termination future to complete and close the surface with
- // the listener that was added after constructing the DeferrableSurface.
- mDeferrableSurface.close();
- mDeferrableSurface.getTerminationFuture().addListener(
- () -> {
- if (releaseVideoEncoder && videoEncoder != null) {
- videoEncoder.release();
- }
- }, CameraXExecutors.mainThreadExecutor());
-
- if (releaseVideoEncoder) {
- mVideoEncoder = null;
- }
- mCameraSurface = null;
- mDeferrableSurface = null;
- }
-
- /**
- * Sets the desired rotation of the output video.
- *
- * <p>In most cases this should be set to the current rotation returned by {@link
- * Display#getRotation()}.
- *
- * @param rotation Desired rotation of the output video.
- */
- public void setTargetRotation(@RotationValue int rotation) {
- setTargetRotationInternal(rotation);
- }
-
- /**
- * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
- * audio from selected audio source.
- */
- @UiThread
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
- VideoCaptureConfig config = (VideoCaptureConfig) getCurrentConfig();
-
- // video encoder setup
- mVideoEncoder.reset();
- mVideoEncoderInitStatus = VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED;
-
- // Configures a Video encoder, if there is any exception, will abort follow up actions
- try {
- mVideoEncoder.configure(
- createVideoMediaFormat(config, resolution), /*surface*/
- null, /*crypto*/
- null,
- MediaCodec.CONFIGURE_FLAG_ENCODE);
- } catch (MediaCodec.CodecException e) {
- int errorCode = 0;
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
- errorCode = Api23Impl.getCodecExceptionErrorCode(e);
- String diagnosticInfo = e.getDiagnosticInfo();
- if (errorCode == MediaCodec.CodecException.ERROR_INSUFFICIENT_RESOURCE) {
- Logger.i(TAG,
- "CodecException: code: " + errorCode + " diagnostic: "
- + diagnosticInfo);
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE;
- } else if (errorCode == MediaCodec.CodecException.ERROR_RECLAIMED) {
- Logger.i(TAG,
- "CodecException: code: " + errorCode + " diagnostic: "
- + diagnosticInfo);
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED;
- }
- } else {
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED;
- }
- mVideoEncoderErrorMessage = e;
- return;
- } catch (IllegalArgumentException | IllegalStateException e) {
- mVideoEncoderInitStatus =
- VideoEncoderInitStatus.VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED;
- mVideoEncoderErrorMessage = e;
- return;
- }
-
- if (mCameraSurface != null) {
- releaseCameraSurface(false);
- }
- Surface cameraSurface = mVideoEncoder.createInputSurface();
- mCameraSurface = cameraSurface;
-
- mSessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-
- if (mDeferrableSurface != null) {
- mDeferrableSurface.close();
- }
- mDeferrableSurface = new ImmediateSurface(mCameraSurface, resolution, getImageFormat());
- mDeferrableSurface.getTerminationFuture().addListener(
- cameraSurface::release, CameraXExecutors.mainThreadExecutor()
- );
-
- mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
-
- mSessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
- @Override
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- public void onError(@NonNull SessionConfig sessionConfig,
- @NonNull SessionConfig.SessionError error) {
- // Ensure the attached camera has not changed before calling setupEncoder.
- // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
- // to this use case so we don't need to do this check.
- if (isCurrentCamera(cameraId)) {
- // Only reset the pipeline when the bound camera is the same.
- setupEncoder(cameraId, resolution);
- notifyReset();
- }
- }
- });
-
- updateSessionConfig(mSessionConfigBuilder.build());
-
- // audio encoder setup
- // reset audio inout flag
- mIsAudioEnabled.set(true);
-
- setAudioParametersByCamcorderProfile(resolution, cameraId);
- mAudioEncoder.reset();
- mAudioEncoder.configure(
- createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
- if (mAudioRecorder != null) {
- mAudioRecorder.release();
- }
- mAudioRecorder = autoConfigAudioRecordSource(config);
- // check mAudioRecorder
- if (mAudioRecorder == null) {
- Logger.e(TAG, "AudioRecord object cannot initialized correctly!");
- mIsAudioEnabled.set(false);
- }
-
- synchronized (mMuxerLock) {
- mVideoTrackIndex = -1;
- mAudioTrackIndex = -1;
- }
- mIsRecording = false;
- }
-
- /**
- * Write a buffer that has been encoded to file.
- *
- * @param bufferIndex the index of the buffer in the videoEncoder that has available data
- * @return returns true if this buffer is the end of the stream
- */
- private boolean writeVideoEncodedBuffer(int bufferIndex) {
- if (bufferIndex < 0) {
- Logger.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
- return false;
- }
- // Get data from buffer
- ByteBuffer outputBuffer = mVideoEncoder.getOutputBuffer(bufferIndex);
-
- // Check if buffer is valid, if not then return
- if (outputBuffer == null) {
- Logger.d(TAG, "OutputBuffer was null.");
- return false;
- }
-
- // Write data to mMuxer if available
- if (mMuxerStarted.get()) {
- if (mVideoBufferInfo.size > 0) {
- outputBuffer.position(mVideoBufferInfo.offset);
- outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
- mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
-
- synchronized (mMuxerLock) {
- if (!mIsFirstVideoKeyFrameWrite.get()) {
- boolean isKeyFrame =
- (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
- if (isKeyFrame) {
- Logger.i(TAG,
- "First video key frame written.");
- mIsFirstVideoKeyFrameWrite.set(true);
- } else {
- // Request a sync frame immediately
- final Bundle syncFrame = new Bundle();
- syncFrame.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
- mVideoEncoder.setParameters(syncFrame);
- }
- }
- mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
- }
- } else {
- Logger.i(TAG, "mVideoBufferInfo.size <= 0, index " + bufferIndex);
- }
- }
-
- // Release data
- mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
-
- // Return true if EOS is set
- return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- }
-
- private boolean writeAudioEncodedBuffer(int bufferIndex) {
- ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
- buffer.position(mAudioBufferInfo.offset);
- if (mMuxerStarted.get()) {
- try {
- if (mAudioBufferInfo.size > 0 && mAudioBufferInfo.presentationTimeUs > 0) {
- synchronized (mMuxerLock) {
- if (!mIsFirstAudioSampleWrite.get()) {
- Logger.i(TAG, "First audio sample written.");
- mIsFirstAudioSampleWrite.set(true);
- }
- mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
- }
- } else {
- Logger.i(TAG, "mAudioBufferInfo size: " + mAudioBufferInfo.size + " "
- + "presentationTimeUs: " + mAudioBufferInfo.presentationTimeUs);
- }
- } catch (Exception e) {
- Logger.e(
- TAG,
- "audio error:size="
- + mAudioBufferInfo.size
- + "/offset="
- + mAudioBufferInfo.offset
- + "/timeUs="
- + mAudioBufferInfo.presentationTimeUs);
- e.printStackTrace();
- }
- }
- mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
- return (mAudioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
- }
-
- /**
- * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
- * main thread otherwise it will cause the application to block.
- *
- * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
- */
- boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
- @NonNull Size resolution,
- @NonNull OutputFileOptions outputFileOptions) {
- // Main encoding loop. Exits on end of stream.
- boolean errorOccurred = false;
- boolean videoEos = false;
- while (!videoEos && !errorOccurred) {
- // Check for end of stream from main thread
- if (mEndOfVideoStreamSignal.get()) {
- mVideoEncoder.signalEndOfInputStream();
- mEndOfVideoStreamSignal.set(false);
- }
-
- // Deque buffer to check for processing step
- int outputBufferId =
- mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
- switch (outputBufferId) {
- case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
- if (mMuxerStarted.get()) {
- videoSavedCallback.onError(
- ERROR_ENCODER,
- "Unexpected change in video encoding format.",
- null);
- errorOccurred = true;
- }
-
- synchronized (mMuxerLock) {
- mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
-
- if ((mIsAudioEnabled.get() && mAudioTrackIndex >= 0
- && mVideoTrackIndex >= 0)
- || (!mIsAudioEnabled.get() && mVideoTrackIndex >= 0)) {
- Logger.i(TAG, "MediaMuxer started on video encode thread and audio "
- + "enabled: " + mIsAudioEnabled);
- mMuxer.start();
- mMuxerStarted.set(true);
- }
- }
- break;
- case MediaCodec.INFO_TRY_AGAIN_LATER:
- // Timed out. Just wait until next attempt to deque.
- break;
- default:
- videoEos = writeVideoEncodedBuffer(outputBufferId);
- }
- }
-
- try {
- Logger.i(TAG, "videoEncoder stop");
- mVideoEncoder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_ENCODER,
- "Video encoder stop failed!", e);
- errorOccurred = true;
- }
-
- try {
- // new MediaMuxer instance required for each new file written, and release current one.
- synchronized (mMuxerLock) {
- if (mMuxer != null) {
- if (mMuxerStarted.get()) {
- Logger.i(TAG, "Muxer already started");
- mMuxer.stop();
- }
- mMuxer.release();
- mMuxer = null;
- }
- }
-
- // A final checking for recording result, if the recorded file has no key
- // frame, then the video file is not playable, needs to call
- // onError() and will be removed.
-
- boolean checkResult = removeRecordingResultIfNoVideoKeyFrameArrived(outputFileOptions);
-
- if (!checkResult) {
- videoSavedCallback.onError(ERROR_RECORDING_TOO_SHORT,
- "The file has no video key frame.", null);
- errorOccurred = true;
- }
- } catch (IllegalStateException e) {
- // The video encoder has not got the key frame yet.
- Logger.i(TAG, "muxer stop IllegalStateException: " + System.currentTimeMillis());
- Logger.i(TAG,
- "muxer stop exception, mIsFirstVideoKeyFrameWrite: "
- + mIsFirstVideoKeyFrameWrite.get());
- if (mIsFirstVideoKeyFrameWrite.get()) {
- // If muxer throws IllegalStateException at this moment and also the key frame
- // has received, this will reported as a Muxer stop failed.
- // Otherwise, this error will be ERROR_RECORDING_TOO_SHORT.
- videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
- } else {
- videoSavedCallback.onError(ERROR_RECORDING_TOO_SHORT,
- "The file has no video key frame.", null);
- }
- errorOccurred = true;
- }
-
- if (mParcelFileDescriptor != null) {
- try {
- mParcelFileDescriptor.close();
- mParcelFileDescriptor = null;
- } catch (IOException e) {
- videoSavedCallback.onError(ERROR_MUXER, "File descriptor close failed!", e);
- errorOccurred = true;
- }
- }
-
- mMuxerStarted.set(false);
-
- // notify the UI thread that the video recording has finished
- mEndOfAudioVideoSignal.set(true);
- mIsFirstVideoKeyFrameWrite.set(false);
-
- Logger.i(TAG, "Video encode thread end.");
- return errorOccurred;
- }
-
- boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
- // Audio encoding loop. Exits on end of stream.
- boolean audioEos = false;
- int outIndex;
- long lastAudioTimestamp = 0;
- while (!audioEos && mIsRecording) {
- // Check for end of stream from main thread
- if (mEndOfAudioStreamSignal.get()) {
- mEndOfAudioStreamSignal.set(false);
- mIsRecording = false;
- }
-
- // get audio deque input buffer
- if (mAudioEncoder != null && mAudioRecorder != null) {
- try {
- int index = mAudioEncoder.dequeueInputBuffer(-1);
- if (index >= 0) {
- final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
- buffer.clear();
- int length = mAudioRecorder.read(buffer, mAudioBufferSize);
- if (length > 0) {
- mAudioEncoder.queueInputBuffer(
- index,
- 0,
- length,
- (System.nanoTime() / 1000),
- mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- }
- }
- } catch (MediaCodec.CodecException e) {
- Logger.i(TAG, "audio dequeueInputBuffer CodecException " + e.getMessage());
- } catch (IllegalStateException e) {
- Logger.i(TAG,
- "audio dequeueInputBuffer IllegalStateException " + e.getMessage());
- }
-
- // start to dequeue audio output buffer
- do {
- outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
- switch (outIndex) {
- case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
- synchronized (mMuxerLock) {
- mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
- if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
- Logger.i(TAG, "MediaMuxer start on audio encoder thread.");
- mMuxer.start();
- mMuxerStarted.set(true);
- }
- }
- break;
- case MediaCodec.INFO_TRY_AGAIN_LATER:
- break;
- default:
- // Drops out of order audio frame if the frame's earlier than last
- // frame.
- if (mAudioBufferInfo.presentationTimeUs > lastAudioTimestamp) {
- audioEos = writeAudioEncodedBuffer(outIndex);
- lastAudioTimestamp = mAudioBufferInfo.presentationTimeUs;
- } else {
- Logger.w(TAG,
- "Drops frame, current frame's timestamp "
- + mAudioBufferInfo.presentationTimeUs
- + " is earlier that last frame "
- + lastAudioTimestamp);
- // Releases this frame from output buffer
- mAudioEncoder.releaseOutputBuffer(outIndex, false);
- }
- }
- } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
- }
- } // end of while loop
-
- // Audio Stop
- try {
- Logger.i(TAG, "audioRecorder stop");
- mAudioRecorder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(
- ERROR_ENCODER, "Audio recorder stop failed!", e);
- }
-
- try {
- mAudioEncoder.stop();
- } catch (IllegalStateException e) {
- videoSavedCallback.onError(ERROR_ENCODER,
- "Audio encoder stop failed!", e);
- }
-
- Logger.i(TAG, "Audio encode thread end");
- // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
- // safe
- mEndOfVideoStreamSignal.set(true);
-
- return false;
- }
-
- private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
- return codec.getInputBuffer(index);
- }
-
- private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
- return codec.getOutputBuffer(index);
- }
-
- /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
- private MediaFormat createAudioMediaFormat() {
- MediaFormat format =
- MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, mAudioSampleRate,
- mAudioChannelCount);
- format.setInteger(
- MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
- format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
-
- return format;
- }
-
- /** Create a AudioRecord object to get raw data */
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- private AudioRecord autoConfigAudioRecordSource(VideoCaptureConfig config) {
- // Use channel count to determine stereo vs mono
- int channelConfig =
- mAudioChannelCount == 1
- ? AudioFormat.CHANNEL_IN_MONO
- : AudioFormat.CHANNEL_IN_STEREO;
-
- try {
- // Use only ENCODING_PCM_16BIT because it mandatory supported.
- int bufferSize =
- AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig,
- AudioFormat.ENCODING_PCM_16BIT);
-
- if (bufferSize <= 0) {
- bufferSize = config.getAudioMinBufferSize();
- }
-
- AudioRecord recorder =
- new AudioRecord(
- AudioSource.CAMCORDER,
- mAudioSampleRate,
- channelConfig,
- AudioFormat.ENCODING_PCM_16BIT,
- bufferSize * 2);
-
- if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
- mAudioBufferSize = bufferSize;
- Logger.i(
- TAG,
- "source: "
- + AudioSource.CAMCORDER
- + " audioSampleRate: "
- + mAudioSampleRate
- + " channelConfig: "
- + channelConfig
- + " audioFormat: "
- + AudioFormat.ENCODING_PCM_16BIT
- + " bufferSize: "
- + bufferSize);
- return recorder;
- }
- } catch (Exception e) {
- Logger.e(TAG, "Exception, keep trying.", e);
- }
- return null;
- }
-
- /** Set audio record parameters by CamcorderProfile */
- @SuppressWarnings("deprecation")
- private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
- CamcorderProfile profile;
- boolean isCamcorderProfileFound = false;
-
- try {
- for (int quality : CamcorderQuality) {
- if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
- profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
- if (currentResolution.getWidth() == profile.videoFrameWidth
- && currentResolution.getHeight() == profile.videoFrameHeight) {
- mAudioChannelCount = profile.audioChannels;
- mAudioSampleRate = profile.audioSampleRate;
- mAudioBitRate = profile.audioBitRate;
- isCamcorderProfileFound = true;
- break;
- }
- }
- }
- } catch (NumberFormatException e) {
- Logger.i(TAG, "The camera Id is not an integer because the camera may be a removable "
- + "device. Use the default values for the audio related settings.");
- }
-
- // In case no corresponding camcorder profile can be founded, * get default value from
- // VideoCaptureConfig.
- if (!isCamcorderProfileFound) {
- VideoCaptureConfig config = (VideoCaptureConfig) getCurrentConfig();
- mAudioChannelCount = config.getAudioChannelCount();
- mAudioSampleRate = config.getAudioSampleRate();
- mAudioBitRate = config.getAudioBitRate();
- }
- }
-
- private boolean removeRecordingResultIfNoVideoKeyFrameArrived(
- @NonNull OutputFileOptions outputFileOptions) {
- boolean checkKeyFrame;
-
- // 1. There should be one video key frame at least.
- Logger.i(TAG,
- "check Recording Result First Video Key Frame Write: "
- + mIsFirstVideoKeyFrameWrite.get());
- if (!mIsFirstVideoKeyFrameWrite.get()) {
- Logger.i(TAG, "The recording result has no key frame.");
- checkKeyFrame = false;
- } else {
- checkKeyFrame = true;
- }
-
- // 2. If no key frame, remove file except the target is a file descriptor case.
- if (outputFileOptions.isSavingToFile()) {
- File outputFile = outputFileOptions.getFile();
- if (!checkKeyFrame) {
- Logger.i(TAG, "Delete file.");
- outputFile.delete();
- }
- } else if (outputFileOptions.isSavingToMediaStore()) {
- if (!checkKeyFrame) {
- Logger.i(TAG, "Delete file.");
- if (mSavedVideoUri != null) {
- ContentResolver contentResolver = outputFileOptions.getContentResolver();
- contentResolver.delete(mSavedVideoUri, null, null);
- }
- }
- }
-
- return checkKeyFrame;
- }
-
- @NonNull
- private MediaMuxer initMediaMuxer(@NonNull OutputFileOptions outputFileOptions)
- throws IOException {
- MediaMuxer mediaMuxer;
-
- if (outputFileOptions.isSavingToFile()) {
- File savedVideoFile = outputFileOptions.getFile();
- mSavedVideoUri = Uri.fromFile(outputFileOptions.getFile());
-
- mediaMuxer = new MediaMuxer(savedVideoFile.getAbsolutePath(),
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- } else if (outputFileOptions.isSavingToFileDescriptor()) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- throw new IllegalArgumentException("Using a FileDescriptor to record a video is "
- + "only supported for Android 8.0 or above.");
- }
-
- mediaMuxer = Api26Impl.createMediaMuxer(outputFileOptions.getFileDescriptor(),
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- } else if (outputFileOptions.isSavingToMediaStore()) {
- ContentValues values = outputFileOptions.getContentValues() != null
- ? new ContentValues(outputFileOptions.getContentValues())
- : new ContentValues();
-
- mSavedVideoUri = outputFileOptions.getContentResolver().insert(
- outputFileOptions.getSaveCollection(), values);
-
- if (mSavedVideoUri == null) {
- throw new IOException("Invalid Uri!");
- }
-
- // Sine API 26, media muxer could be initiated by a FileDescriptor.
- try {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- String savedLocationPath = VideoUtil.getAbsolutePathFromUri(
- outputFileOptions.getContentResolver(), mSavedVideoUri);
-
- Logger.i(TAG, "Saved Location Path: " + savedLocationPath);
- mediaMuxer = new MediaMuxer(savedLocationPath,
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- } else {
- mParcelFileDescriptor =
- outputFileOptions.getContentResolver().openFileDescriptor(
- mSavedVideoUri, "rw");
- mediaMuxer = Api26Impl.createMediaMuxer(
- mParcelFileDescriptor.getFileDescriptor(),
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
- }
- } catch (IOException e) {
- mSavedVideoUri = null;
- throw e;
- }
- } else {
- throw new IllegalArgumentException(
- "The OutputFileOptions should assign before recording");
- }
-
- return mediaMuxer;
- }
-
- /**
- * Describes the error that occurred during video capture operations.
- *
- * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
- * VideoCapture.OnVideoSavedCallback#onError(int, String, Throwable)}.
- *
- * <p>See message parameter in onError callback or log for more details.
- *
- * @hide
- */
- @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
- ERROR_FILE_IO, ERROR_INVALID_CAMERA, ERROR_RECORDING_TOO_SHORT})
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(Scope.LIBRARY_GROUP)
- public @interface VideoCaptureError {
- }
-
- enum VideoEncoderInitStatus {
- VIDEO_ENCODER_INIT_STATUS_UNINITIALIZED,
- VIDEO_ENCODER_INIT_STATUS_INITIALIZED_FAILED,
- VIDEO_ENCODER_INIT_STATUS_INSUFFICIENT_RESOURCE,
- VIDEO_ENCODER_INIT_STATUS_RESOURCE_RECLAIMED,
- }
-
- /** Listener containing callbacks for video file I/O events. */
- public interface OnVideoSavedCallback {
- /** Called when the video has been successfully saved. */
- void onVideoSaved(@NonNull OutputFileResults outputFileResults);
-
- /** Called when an error occurs while attempting to save the video. */
- void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
- @Nullable Throwable cause);
- }
-
- /**
- * Provides a base static default configuration for the VideoCapture
- *
- * <p>These values may be overridden by the implementation. They only provide a minimum set of
- * defaults that are implementation independent.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- public static final class Defaults
- implements ConfigProvider<VideoCaptureConfig> {
- private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
- /** 8Mb/s the recommend rate for 30fps 1080p */
- private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
- /** Seconds between each key frame */
- private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
- /** audio bit rate */
- private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
- /** audio sample rate */
- private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
- /** audio channel count */
- private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
- /** audio default minimum buffer size */
- private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
- /** Current max resolution of VideoCapture is set as FHD */
- private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
- /** Surface occupancy priority to this use case */
- private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
- private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_16_9;
-
- private static final VideoCaptureConfig DEFAULT_CONFIG;
-
- static {
- Builder builder = new Builder()
- .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
- .setBitRate(DEFAULT_BIT_RATE)
- .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
- .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
- .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
- .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
- .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
- .setMaxResolution(DEFAULT_MAX_RESOLUTION)
- .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
- .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
-
- DEFAULT_CONFIG = builder.getUseCaseConfig();
- }
-
- @NonNull
- @Override
- public VideoCaptureConfig getConfig() {
- return DEFAULT_CONFIG;
- }
- }
-
- /** Holder class for metadata that should be saved alongside captured video. */
- public static final class Metadata {
- /** Data representing a geographic location. */
- @Nullable
- public Location location;
- }
-
- private static final class VideoSavedListenerWrapper implements OnVideoSavedCallback {
-
- @NonNull
- Executor mExecutor;
- @NonNull
- OnVideoSavedCallback mOnVideoSavedCallback;
-
- VideoSavedListenerWrapper(@NonNull Executor executor,
- @NonNull OnVideoSavedCallback onVideoSavedCallback) {
- mExecutor = executor;
- mOnVideoSavedCallback = onVideoSavedCallback;
- }
-
- @Override
- public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
- try {
- mExecutor.execute(() -> mOnVideoSavedCallback.onVideoSaved(outputFileResults));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "Unable to post to the supplied executor.");
- }
- }
-
- @Override
- public void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
- @Nullable Throwable cause) {
- try {
- mExecutor.execute(
- () -> mOnVideoSavedCallback.onError(videoCaptureError, message, cause));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "Unable to post to the supplied executor.");
- }
- }
-
- }
-
- /** Builder for a {@link VideoCapture}. */
- @SuppressWarnings("ObjectToString")
- public static final class Builder
- implements
- UseCaseConfig.Builder<VideoCapture, VideoCaptureConfig, Builder>,
- ImageOutputConfig.Builder<Builder>,
- ThreadConfig.Builder<Builder> {
-
- private final MutableOptionsBundle mMutableConfig;
-
- /** Creates a new Builder object. */
- public Builder() {
- this(MutableOptionsBundle.create());
- }
-
- private Builder(@NonNull MutableOptionsBundle mutableConfig) {
- mMutableConfig = mutableConfig;
-
- Class<?> oldConfigClass =
- mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
- if (oldConfigClass != null && !oldConfigClass.equals(VideoCapture.class)) {
- throw new IllegalArgumentException(
- "Invalid target class configuration for "
- + Builder.this
- + ": "
- + oldConfigClass);
- }
-
- setTargetClass(VideoCapture.class);
- }
-
- /**
- * Generates a Builder from another Config object.
- *
- * @param configuration An immutable configuration to pre-populate this builder.
- * @return The new Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- static Builder fromConfig(@NonNull Config configuration) {
- return new Builder(MutableOptionsBundle.from(configuration));
- }
-
-
- /**
- * Generates a Builder from another Config object
- *
- * @param configuration An immutable configuration to pre-populate this builder.
- * @return The new Builder.
- */
- @NonNull
- public static Builder fromConfig(@NonNull VideoCaptureConfig configuration) {
- return new Builder(MutableOptionsBundle.from(configuration));
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public MutableConfig getMutableConfig() {
- return mMutableConfig;
- }
-
- /**
- * {@inheritDoc}
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public VideoCaptureConfig getUseCaseConfig() {
- return new VideoCaptureConfig(OptionsBundle.from(mMutableConfig));
- }
-
- /**
- * Builds an immutable {@link VideoCaptureConfig} from the current state.
- *
- * @return A {@link VideoCaptureConfig} populated with the current state.
- */
- @Override
- @NonNull
- public VideoCapture build() {
- VideoCaptureConfig videoCaptureConfig = getUseCaseConfig();
- ImageOutputConfig.validateConfig(videoCaptureConfig);
- return new VideoCapture(videoCaptureConfig);
- }
-
- /**
- * Sets the recording frames per second.
- *
- * @param videoFrameRate The requested interval in seconds.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setVideoFrameRate(int videoFrameRate) {
- getMutableConfig().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
- return this;
- }
-
- /**
- * Sets the encoding bit rate.
- *
- * @param bitRate The requested bit rate in bits per second.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setBitRate(int bitRate) {
- getMutableConfig().insertOption(OPTION_BIT_RATE, bitRate);
- return this;
- }
-
- /**
- * Sets number of seconds between each key frame in seconds.
- *
- * @param interval The requested interval in seconds.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setIFrameInterval(int interval) {
- getMutableConfig().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
- return this;
- }
-
- /**
- * Sets the bit rate of the audio stream.
- *
- * @param bitRate The requested bit rate in bits/s.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioBitRate(int bitRate) {
- getMutableConfig().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
- return this;
- }
-
- /**
- * Sets the sample rate of the audio stream.
- *
- * @param sampleRate The requested sample rate in bits/s.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioSampleRate(int sampleRate) {
- getMutableConfig().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
- return this;
- }
-
- /**
- * Sets the number of audio channels.
- *
- * @param channelCount The requested number of audio channels.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioChannelCount(int channelCount) {
- getMutableConfig().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
- return this;
- }
-
- /**
- * Sets the audio min buffer size.
- *
- * @param minBufferSize The requested audio minimum buffer size, in bytes.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setAudioMinBufferSize(int minBufferSize) {
- getMutableConfig().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
- return this;
- }
-
- // Implementations of TargetConfig.Builder default methods
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setTargetClass(@NonNull Class<VideoCapture> targetClass) {
- getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
-
- // If no name is set yet, then generate a unique name
- if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
- String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
- setTargetName(targetName);
- }
-
- return this;
- }
-
- /**
- * Sets the name of the target object being configured, used only for debug logging.
- *
- * <p>The name should be a value that can uniquely identify an instance of the object being
- * configured.
- *
- * <p>If not set, the target name will default to an unique name automatically generated
- * with the class canonical name and random UUID.
- *
- * @param targetName A unique string identifier for the instance of the class being
- * configured.
- * @return the current Builder.
- */
- @Override
- @NonNull
- public Builder setTargetName(@NonNull String targetName) {
- getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
- return this;
- }
-
- // Implementations of ImageOutputConfig.Builder default methods
-
- /**
- * Sets the aspect ratio of the intended target for images from this configuration.
- *
- * <p>It is not allowed to set both target aspect ratio and target resolution on the same
- * use case.
- *
- * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
- * ratio which may differ from the request, possibly due to device constraints.
- * Application code should check the resulting output's resolution.
- *
- * <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
- * priority.
- *
- * @param aspectRatio A {@link AspectRatio} representing the ratio of the
- * target's width and height.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
- getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
- return this;
- }
-
- /**
- * Sets the rotation of the intended target for images from this configuration.
- *
- * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
- * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
- * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
- *
- * <p>If not set, the target rotation will default to the value of
- * {@link Display#getRotation()} of the default display at the time the use case is
- * created. The use case is fully created once it has been attached to a camera.
- *
- * @param rotation The rotation of the intended target.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setTargetRotation(@RotationValue int rotation) {
- getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
- return this;
- }
-
- /**
- * Sets the resolution of the intended target from this configuration.
- *
- * <p>The target resolution attempts to establish a minimum bound for the image resolution.
- * The actual image resolution will be the closest available resolution in size that is not
- * smaller than the target resolution, as determined by the Camera implementation. However,
- * if no resolution exists that is equal to or larger than the target resolution, the
- * nearest available resolution smaller than the target resolution will be chosen.
- *
- * <p>It is not allowed to set both target aspect ratio and target resolution on the same
- * use case.
- *
- * <p>The target aspect ratio will also be set the same as the aspect ratio of the provided
- * {@link Size}. Make sure to set the target resolution with the correct orientation.
- *
- * @param resolution The target resolution to choose from supported output sizes list.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setTargetResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
- return this;
- }
-
- /**
- * Sets the default resolution of the intended target from this configuration.
- *
- * @param resolution The default resolution to choose from supported output sizes list.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setDefaultResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setMaxResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
- getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
- getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
- return this;
- }
-
- // Implementations of ThreadConfig.Builder default methods
-
- /**
- * Sets the default executor that will be used for background tasks.
- *
- * <p>If not set, the background executor will default to an automatically generated
- * {@link Executor}.
- *
- * @param executor The executor which will be used for background tasks.
- * @return the current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setBackgroundExecutor(@NonNull Executor executor) {
- getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
- return this;
- }
-
- // Implementations of UseCaseConfig.Builder default methods
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
- getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
- getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setSessionOptionUnpacker(
- @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
- getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setCaptureOptionUnpacker(
- @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
- getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setSurfaceOccupancyPriority(int priority) {
- getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY)
- @Override
- @NonNull
- public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
- getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @NonNull
- public Builder setUseCaseEventCallback(
- @NonNull UseCase.EventCallback useCaseEventCallback) {
- getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setZslDisabled(boolean disabled) {
- getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
- return this;
- }
-
- /** @hide */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- @Override
- public Builder setHighResolutionDisabled(boolean disabled) {
- getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
- return this;
- }
- }
-
- /**
- * Info about the saved video file.
- */
- public static class OutputFileResults {
- @Nullable
- private Uri mSavedUri;
-
- OutputFileResults(@Nullable Uri savedUri) {
- mSavedUri = savedUri;
- }
-
- /**
- * Returns the {@link Uri} of the saved video file.
- *
- * <p> This field is only returned if the {@link VideoCapture.OutputFileOptions} is
- * backed by {@link MediaStore} constructed with
- * {@link androidx.camera.core.VideoCapture.OutputFileOptions}.
- */
- @Nullable
- public Uri getSavedUri() {
- return mSavedUri;
- }
- }
-
- /**
- * Options for saving newly captured video.
- *
- * <p> this class is used to configure save location and metadata. Save location can be
- * either a {@link File}, {@link MediaStore}. The metadata will be
- * stored with the saved video.
- */
- public static final class OutputFileOptions {
-
- // Empty metadata object used as a placeholder for no user-supplied metadata.
- // Should be initialized to all default values.
- private static final Metadata EMPTY_METADATA = new Metadata();
-
- @Nullable
- private final File mFile;
- @Nullable
- private final FileDescriptor mFileDescriptor;
- @Nullable
- private final ContentResolver mContentResolver;
- @Nullable
- private final Uri mSaveCollection;
- @Nullable
- private final ContentValues mContentValues;
- @Nullable
- private final Metadata mMetadata;
-
- OutputFileOptions(@Nullable File file,
- @Nullable FileDescriptor fileDescriptor,
- @Nullable ContentResolver contentResolver,
- @Nullable Uri saveCollection,
- @Nullable ContentValues contentValues,
- @Nullable Metadata metadata) {
- mFile = file;
- mFileDescriptor = fileDescriptor;
- mContentResolver = contentResolver;
- mSaveCollection = saveCollection;
- mContentValues = contentValues;
- mMetadata = metadata == null ? EMPTY_METADATA : metadata;
- }
-
- /** Returns the File object which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- File getFile() {
- return mFile;
- }
-
- /**
- * Returns the FileDescriptor object which is set by the {@link OutputFileOptions.Builder}.
- */
- @Nullable
- FileDescriptor getFileDescriptor() {
- return mFileDescriptor;
- }
-
- /** Returns the content resolver which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- ContentResolver getContentResolver() {
- return mContentResolver;
- }
-
- /** Returns the URI which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- Uri getSaveCollection() {
- return mSaveCollection;
- }
-
- /** Returns the content values which is set by the {@link OutputFileOptions.Builder}. */
- @Nullable
- ContentValues getContentValues() {
- return mContentValues;
- }
-
- /** Return the metadata which is set by the {@link OutputFileOptions.Builder}.. */
- @Nullable
- Metadata getMetadata() {
- return mMetadata;
- }
-
- /** Checking the caller wants to save video to MediaStore. */
- boolean isSavingToMediaStore() {
- return getSaveCollection() != null && getContentResolver() != null
- && getContentValues() != null;
- }
-
- /** Checking the caller wants to save video to a File. */
- boolean isSavingToFile() {
- return getFile() != null;
- }
-
- /** Checking the caller wants to save video to a FileDescriptor. */
- boolean isSavingToFileDescriptor() {
- return getFileDescriptor() != null;
- }
-
- /**
- * Builder class for {@link OutputFileOptions}.
- */
- public static final class Builder {
- @Nullable
- private File mFile;
- @Nullable
- private FileDescriptor mFileDescriptor;
- @Nullable
- private ContentResolver mContentResolver;
- @Nullable
- private Uri mSaveCollection;
- @Nullable
- private ContentValues mContentValues;
- @Nullable
- private Metadata mMetadata;
-
- /**
- * Creates options to write captured video to a {@link File}.
- *
- * @param file save location of the video.
- */
- public Builder(@NonNull File file) {
- mFile = file;
- }
-
- /**
- * Creates options to write captured video to a {@link FileDescriptor}.
- *
- * <p>Using a FileDescriptor to record a video is only supported for Android 8.0 or
- * above.
- *
- * @param fileDescriptor to save the video.
- * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
- */
- public Builder(@NonNull FileDescriptor fileDescriptor) {
- Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
- "Using a FileDescriptor to record a video is only supported for Android 8"
- + ".0 or above.");
-
- mFileDescriptor = fileDescriptor;
- }
-
- /**
- * Creates options to write captured video to {@link MediaStore}.
- *
- * Example:
- *
- * <pre>{@code
- *
- * ContentValues contentValues = new ContentValues();
- * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
- * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
- *
- * OutputFileOptions options = new OutputFileOptions.Builder(
- * getContentResolver(),
- * MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
- * contentValues).build();
- *
- * }</pre>
- *
- * @param contentResolver to access {@link MediaStore}
- * @param saveCollection The URL of the table to insert into.
- * @param contentValues to be included in the created video file.
- */
- public Builder(@NonNull ContentResolver contentResolver,
- @NonNull Uri saveCollection,
- @NonNull ContentValues contentValues) {
- mContentResolver = contentResolver;
- mSaveCollection = saveCollection;
- mContentValues = contentValues;
- }
-
- /**
- * Sets the metadata to be stored with the saved video.
- *
- * @param metadata Metadata to be stored with the saved video.
- */
- @NonNull
- public Builder setMetadata(@NonNull Metadata metadata) {
- mMetadata = metadata;
- return this;
- }
-
- /**
- * Builds {@link OutputFileOptions}.
- */
- @NonNull
- public OutputFileOptions build() {
- return new OutputFileOptions(mFile, mFileDescriptor, mContentResolver,
- mSaveCollection, mContentValues, mMetadata);
- }
- }
- }
-
- /**
- * Nested class to avoid verification errors for methods introduced in Android 8.0 (API 26).
- */
- @RequiresApi(26)
- private static class Api26Impl {
-
- private Api26Impl() {
- }
-
- @DoNotInline
- @NonNull
- static MediaMuxer createMediaMuxer(@NonNull FileDescriptor fileDescriptor, int format)
- throws IOException {
- return new MediaMuxer(fileDescriptor, format);
- }
- }
-
- /**
- * Nested class to avoid verification errors for methods introduced in Android 6.0 (API 23).
- */
- @RequiresApi(23)
- private static class Api23Impl {
-
- private Api23Impl() {
- }
-
- @DoNotInline
- static int getCodecExceptionErrorCode(MediaCodec.CodecException e) {
- return e.getErrorCode();
- }
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
index c7daab2..3113c4d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Image2JpegBytes.java
@@ -17,31 +17,26 @@
package androidx.camera.core.imagecapture;
import static android.graphics.ImageFormat.JPEG;
-import static android.graphics.ImageFormat.NV21;
import static android.graphics.ImageFormat.YUV_420_888;
import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
import static androidx.camera.core.impl.utils.Exif.createFromInputStream;
import static androidx.camera.core.impl.utils.TransformUtils.updateSensorToBufferTransform;
import static androidx.camera.core.internal.utils.ImageUtil.jpegImageToJpegByteArray;
-import static androidx.camera.core.internal.utils.ImageUtil.yuv_420_888toNv21;
-import static java.nio.ByteBuffer.allocateDirect;
import static java.util.Objects.requireNonNull;
import android.graphics.Rect;
-import android.graphics.YuvImage;
import android.os.Build;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.utils.Exif;
-import androidx.camera.core.impl.utils.ExifData;
-import androidx.camera.core.impl.utils.ExifOutputStream;
-import androidx.camera.core.internal.ByteBufferOutputStream;
+import androidx.camera.core.internal.utils.ImageUtil;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -49,8 +44,6 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
/**
* Converts a {@link ImageProxy} to JPEG bytes.
@@ -95,16 +88,17 @@
ImageProxy image = packet.getData();
Rect cropRect = packet.getCropRect();
- // Converts YUV_420_888 to NV21.
- byte[] yuvBytes = yuv_420_888toNv21(image);
- YuvImage yuvImage = new YuvImage(yuvBytes, NV21, image.getWidth(), image.getHeight(), null);
-
- // Compress NV21 to JPEG and crop.
- ByteBuffer buffer = allocateDirect(cropRect.width() * cropRect.height() * 2);
- OutputStream outputStream = new ExifOutputStream(new ByteBufferOutputStream(buffer),
- ExifData.create(image, packet.getRotationDegrees()));
- yuvImage.compressToJpeg(cropRect, input.getJpegQuality(), outputStream);
- byte[] jpegBytes = byteBufferToByteArray(buffer);
+ byte[] jpegBytes;
+ try {
+ jpegBytes = ImageUtil.yuvImageToJpegByteArray(
+ image,
+ cropRect,
+ input.getJpegQuality(),
+ packet.getRotationDegrees());
+ } catch (ImageUtil.CodecFailedException e) {
+ throw new ImageCaptureException(ImageCapture.ERROR_FILE_IO,
+ "Failed to encode the image to JPEG.", e);
+ }
// Return bytes with a new format, size, and crop rect.
return Packet.of(
@@ -118,14 +112,6 @@
packet.getCameraCaptureResult());
}
- private static byte[] byteBufferToByteArray(@NonNull ByteBuffer buffer) {
- int jpegSize = buffer.position();
- byte[] bytes = new byte[jpegSize];
- buffer.rewind();
- buffer.get(bytes, 0, jpegSize);
- return bytes;
- }
-
private static Exif extractExif(@NonNull byte[] jpegBytes) throws ImageCaptureException {
try {
return createFromInputStream(new ByteArrayInputStream(jpegBytes));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
index a9a7602..eb45dd8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
@@ -47,8 +47,6 @@
Option.create("camerax.core.imageCapture.flashMode", int.class);
public static final Option<CaptureBundle> OPTION_CAPTURE_BUNDLE =
Option.create("camerax.core.imageCapture.captureBundle", CaptureBundle.class);
- public static final Option<CaptureProcessor> OPTION_CAPTURE_PROCESSOR =
- Option.create("camerax.core.imageCapture.captureProcessor", CaptureProcessor.class);
public static final Option<Integer> OPTION_BUFFER_FORMAT =
Option.create("camerax.core.imageCapture.bufferFormat", Integer.class);
public static final Option<Integer> OPTION_MAX_CAPTURE_STAGES =
@@ -144,29 +142,6 @@
}
/**
- * Returns the {@link CaptureProcessor}.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- @Nullable
- public CaptureProcessor getCaptureProcessor(@Nullable CaptureProcessor valueIfMissing) {
- return retrieveOption(OPTION_CAPTURE_PROCESSOR, valueIfMissing);
- }
-
- /**
- * Returns the {@link CaptureProcessor}.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- @NonNull
- public CaptureProcessor getCaptureProcessor() {
- return retrieveOption(OPTION_CAPTURE_PROCESSOR);
- }
-
- /**
* Returns the {@link ImageFormat} of the capture in memory.
*
* @param valueIfMissing The value to return if this configuration option has not been set.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
index 6d235ab..6a3ee7a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
@@ -17,7 +17,6 @@
package androidx.camera.core.impl;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Preview;
import androidx.camera.core.internal.ThreadConfig;
@@ -32,13 +31,6 @@
ThreadConfig {
// Options declarations
-
- public static final Option<ImageInfoProcessor> IMAGE_INFO_PROCESSOR = Option.create(
- "camerax.core.preview.imageInfoProcessor", ImageInfoProcessor.class);
- public static final Option<CaptureProcessor> OPTION_PREVIEW_CAPTURE_PROCESSOR =
- Option.create("camerax.core.preview.captureProcessor", CaptureProcessor.class);
- public static final Option<Boolean> OPTION_RGBA8888_SURFACE_REQUIRED =
- Option.create("camerax.core.preview.isRgba8888SurfaceRequired", Boolean.class);
private final OptionsBundle mConfig;
/** Creates a new configuration instance. */
@@ -53,50 +45,6 @@
}
/**
- * Returns the {@link ImageInfoProcessor}.
- *
- * @return The stored value, if it exists in this configuration.
- */
- @Nullable
- public ImageInfoProcessor getImageInfoProcessor(@Nullable ImageInfoProcessor valueIfMissing) {
- return retrieveOption(IMAGE_INFO_PROCESSOR, valueIfMissing);
- }
-
- @NonNull
- ImageInfoProcessor getImageInfoProcessor() {
- return retrieveOption(IMAGE_INFO_PROCESSOR);
- }
-
- /**
- * Returns the {@link CaptureProcessor}.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- @Nullable
- public CaptureProcessor getCaptureProcessor(@Nullable CaptureProcessor valueIfMissing) {
- return retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, valueIfMissing);
- }
-
- /**
- * Returns the {@link CaptureProcessor}.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- @NonNull
- public CaptureProcessor getCaptureProcessor() {
- return retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR);
- }
-
- /**
- * Returns if the preview surface requires RGBA8888 format.
- */
- public boolean isRgba8888SurfaceRequired(boolean valueIfMissing) {
- return retrieveOption(OPTION_RGBA8888_SURFACE_REQUIRED, valueIfMissing);
- }
- /**
* Retrieves the format of the image that is fed as input.
*
* <p>This should be YUV_420_888, when processing is run on the image. Otherwise it is PRIVATE.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
index ef6c237..e47c72a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/RequestProcessor.java
@@ -97,30 +97,30 @@
* Callback to be invoked during the capture.
*/
interface Callback {
- void onCaptureStarted(
+ default void onCaptureStarted(
@NonNull Request request,
long frameNumber,
- long timestamp);
+ long timestamp) {}
- void onCaptureProgressed(
+ default void onCaptureProgressed(
@NonNull Request request,
- @NonNull CameraCaptureResult captureResult);
+ @NonNull CameraCaptureResult captureResult) {}
- void onCaptureCompleted(
+ default void onCaptureCompleted(
@NonNull Request request,
- @NonNull CameraCaptureResult captureResult);
+ @NonNull CameraCaptureResult captureResult) {}
- void onCaptureFailed(
+ default void onCaptureFailed(
@NonNull Request request,
- @NonNull CameraCaptureFailure captureFailure);
+ @NonNull CameraCaptureFailure captureFailure) {}
- void onCaptureBufferLost(
+ default void onCaptureBufferLost(
@NonNull Request request,
long frameNumber,
- int outputConfigId);
+ int outputConfigId) {}
- void onCaptureSequenceCompleted(int sequenceId, long frameNumber);
+ default void onCaptureSequenceCompleted(int sequenceId, long frameNumber) {}
- void onCaptureSequenceAborted(int sequenceId);
+ default void onCaptureSequenceAborted(int sequenceId) {}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index bb86a51..f9858f5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -120,6 +120,13 @@
void abortCapture(int captureSequenceId);
/**
+ * Sends trigger-type single request such as AF/AE triggers.
+ */
+ default int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
+ return -1;
+ }
+
+ /**
* Callback for {@link #startRepeating} and {@link #startCapture}.
*/
interface CaptureCallback {
@@ -136,7 +143,7 @@
* request or the timestamp at start of capture of the
* first frame in a multi-frame capture, in nanoseconds.
*/
- void onCaptureStarted(int captureSequenceId, long timestamp);
+ default void onCaptureStarted(int captureSequenceId, long timestamp) {}
/**
* This method is called when an image (or images in case of multi-frame
@@ -144,7 +151,7 @@
*
* @param captureSequenceId id of the current capture sequence
*/
- void onCaptureProcessStarted(int captureSequenceId);
+ default void onCaptureProcessStarted(int captureSequenceId) {}
/**
* This method is called instead of {@link #onCaptureProcessStarted} when the camera
@@ -153,7 +160,7 @@
*
* @param captureSequenceId id of the current capture sequence
*/
- void onCaptureFailed(int captureSequenceId);
+ default void onCaptureFailed(int captureSequenceId) {}
/**
* This method is called independently of the others in the CaptureCallback, when a capture
@@ -166,14 +173,14 @@
*
* @param captureSequenceId id of the current capture sequence
*/
- void onCaptureSequenceCompleted(int captureSequenceId);
+ default void onCaptureSequenceCompleted(int captureSequenceId) {}
/**
* This method is called when a capture sequence aborts.
*
* @param captureSequenceId id of the current capture sequence
*/
- void onCaptureSequenceAborted(int captureSequenceId);
+ default void onCaptureSequenceAborted(int captureSequenceId) {}
/**
* Capture result callback that needs to be called when the process capture results are
@@ -197,7 +204,7 @@
* that those two settings and results are always supported and
* applied by the corresponding framework.
*/
- void onCaptureCompleted(long timestamp, int captureSequenceId,
- @NonNull Map<CaptureResult.Key, Object> result);
+ default void onCaptureCompleted(long timestamp, int captureSequenceId,
+ @NonNull Map<CaptureResult.Key, Object> result) {}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java
deleted file mode 100644
index db1c480..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/VideoCaptureConfig.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core.impl;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.internal.ThreadConfig;
-
-/**
- * Config for a video capture use case.
- *
- * <p>In the earlier stage, the VideoCapture is deprioritized.
- */
-@SuppressWarnings("deprecation")
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class VideoCaptureConfig
- implements UseCaseConfig<androidx.camera.core.VideoCapture>,
- ImageOutputConfig,
- ThreadConfig {
-
- // Option Declarations:
- // *********************************************************************************************
-
- public static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
- Option.create("camerax.core.videoCapture.recordingFrameRate", int.class);
- public static final Option<Integer> OPTION_BIT_RATE =
- Option.create("camerax.core.videoCapture.bitRate", int.class);
- public static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
- Option.create("camerax.core.videoCapture.intraFrameInterval", int.class);
- public static final Option<Integer> OPTION_AUDIO_BIT_RATE =
- Option.create("camerax.core.videoCapture.audioBitRate", int.class);
- public static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
- Option.create("camerax.core.videoCapture.audioSampleRate", int.class);
- public static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
- Option.create("camerax.core.videoCapture.audioChannelCount", int.class);
- public static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
- Option.create("camerax.core.videoCapture.audioMinBufferSize", int.class);
-
- // *********************************************************************************************
-
- private final OptionsBundle mConfig;
-
- public VideoCaptureConfig(@NonNull OptionsBundle config) {
- mConfig = config;
- }
-
- /**
- * Returns the recording frames per second.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getVideoFrameRate(int valueIfMissing) {
- return retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
- }
-
- /**
- * Returns the recording frames per second.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getVideoFrameRate() {
- return retrieveOption(OPTION_VIDEO_FRAME_RATE);
- }
-
- /**
- * Returns the encoding bit rate.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getBitRate(int valueIfMissing) {
- return retrieveOption(OPTION_BIT_RATE, valueIfMissing);
- }
-
- /**
- * Returns the encoding bit rate.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getBitRate() {
- return retrieveOption(OPTION_BIT_RATE);
- }
-
- /**
- * Returns the number of seconds between each key frame.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getIFrameInterval(int valueIfMissing) {
- return retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
- }
-
- /**
- * Returns the number of seconds between each key frame.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getIFrameInterval() {
- return retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
- }
-
- /**
- * Returns the audio encoding bit rate.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioBitRate(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
- }
-
- /**
- * Returns the audio encoding bit rate.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioBitRate() {
- return retrieveOption(OPTION_AUDIO_BIT_RATE);
- }
-
- /**
- * Returns the audio sample rate.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioSampleRate(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
- }
-
- /**
- * Returns the audio sample rate.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioSampleRate() {
- return retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
- }
-
- /**
- * Returns the audio channel count.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioChannelCount(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
- }
-
- /**
- * Returns the audio channel count.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioChannelCount() {
- return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
- }
-
- /**
- * Returns the audio minimum buffer size, in bytes.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- public int getAudioMinBufferSize(int valueIfMissing) {
- return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
- }
-
- /**
- * Returns the audio minimum buffer size, in bytes.
- *
- * @return The stored value, if it exists in this configuration.
- * @throws IllegalArgumentException if the option does not exist in this configuration.
- */
- public int getAudioMinBufferSize() {
- return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
- }
-
- /**
- * Retrieves the format of the image that is fed as input.
- *
- * <p>This should always be PRIVATE for VideoCapture.
- */
- @Override
- public int getInputFormat() {
- return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
- }
-
- @NonNull
- @Override
- public Config getConfig() {
- return mConfig;
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
index f8ebadf..36330a5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
@@ -308,7 +308,9 @@
public static ExifData create(@NonNull ImageProxy imageProxy,
@ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
ExifData.Builder builder = ExifData.builderForDevice();
- imageProxy.getImageInfo().populateExifData(builder);
+ if (imageProxy.getImageInfo() != null) {
+ imageProxy.getImageInfo().populateExifData(builder);
+ }
// Overwrites the orientation degrees value of the output image because the capture
// results might not have correct value when capturing image in YUV_420_888 format. See
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 75679d4..081ccc1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.internal.utils.ImageUtil;
import androidx.core.util.Preconditions;
import java.util.Locale;
@@ -36,7 +37,7 @@
* {@link RectF}, a rotation degrees integer and a boolean flag for the rotation-direction
* (clockwise v.s. counter-clockwise).
*
- * TODO(b/179827713): merge this with {@link androidx.camera.core.internal.utils.ImageUtil}.
+ * TODO(b/179827713): merge this with {@link ImageUtil}.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class TransformUtils {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
index 9cce3f6..5d79905 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/SurfaceSorter.java
@@ -60,10 +60,8 @@
});
}
- @SuppressWarnings("deprecation")
private int getSurfacePriority(@NonNull DeferrableSurface surface) {
- if (surface.getContainerClass() == MediaCodec.class
- || surface.getContainerClass() == androidx.camera.core.VideoCapture.class) {
+ if (surface.getContainerClass() == MediaCodec.class) {
return PRIORITY_MEDIA_CODEC_SURFACE;
} else if (surface.getContainerClass() == Preview.class) {
return PRIORITY_PREVIEW_SURFACE;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
index 9c8101f..925e953 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2022 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.
@@ -37,9 +37,12 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.ExifData;
+import androidx.camera.core.impl.utils.ExifOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
@@ -163,23 +166,37 @@
/**
* Converts YUV_420_888 {@link ImageProxy} to JPEG byte array. The input YUV_420_888 image
* will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
- * be compressed by the specified quality value.
+ * be compressed by the specified quality value. The rotationDegrees is set to the EXIF of
+ * the JPEG if it is not 0.
*/
@NonNull
public static byte[] yuvImageToJpegByteArray(@NonNull ImageProxy image,
- @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)
- throws CodecFailedException {
+ @Nullable Rect cropRect,
+ @IntRange(from = 1, to = 100)
+ int jpegQuality,
+ int rotationDegrees) throws CodecFailedException {
if (image.getFormat() != ImageFormat.YUV_420_888) {
throw new IllegalArgumentException(
"Incorrect image format of the input image proxy: " + image.getFormat());
}
- return ImageUtil.nv21ToJpeg(
- ImageUtil.yuv_420_888toNv21(image),
- image.getWidth(),
- image.getHeight(),
- cropRect,
- jpegQuality);
+ byte[] yuvBytes = yuv_420_888toNv21(image);
+ YuvImage yuv = new YuvImage(yuvBytes, ImageFormat.NV21, image.getWidth(), image.getHeight(),
+ null);
+
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ OutputStream out = new ExifOutputStream(
+ byteArrayOutputStream, ExifData.create(image, rotationDegrees));
+ if (cropRect == null) {
+ cropRect = new Rect(0, 0, image.getWidth(), image.getHeight());
+ }
+ boolean success =
+ yuv.compressToJpeg(cropRect, jpegQuality, out);
+ if (!success) {
+ throw new CodecFailedException("YuvImage failed to encode jpeg.",
+ CodecFailedException.FailureType.ENCODE_FAILED);
+ }
+ return byteArrayOutputStream.toByteArray();
}
/** {@link android.media.Image} to NV21 byte array. */
@@ -364,21 +381,6 @@
return dispatchCropRect;
}
- private static byte[] nv21ToJpeg(@NonNull byte[] nv21, int width, int height,
- @Nullable Rect cropRect, @IntRange(from = 1, to = 100) int jpegQuality)
- throws CodecFailedException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
- boolean success =
- yuv.compressToJpeg(cropRect == null ? new Rect(0, 0, width, height) : cropRect,
- jpegQuality, out);
- if (!success) {
- throw new CodecFailedException("YuvImage failed to encode jpeg.",
- CodecFailedException.FailureType.ENCODE_FAILED);
- }
- return out.toByteArray();
- }
-
private static boolean isCropAspectRatioHasEffect(@NonNull Size sourceSize,
@NonNull Rational aspectRatio) {
int sourceWidth = sourceSize.getWidth();
@@ -423,7 +425,7 @@
UNKNOWN
}
- private FailureType mFailureType;
+ private final FailureType mFailureType;
CodecFailedException(@NonNull String message) {
super(message);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index efaa57f..de60c70 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -35,6 +35,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ProcessingException;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
import androidx.camera.core.SurfaceRequest;
@@ -59,6 +61,8 @@
@SuppressWarnings("UnusedVariable")
public class SurfaceProcessorNode implements Node<SurfaceEdge, SurfaceEdge> {
+ private static final String TAG = "SurfaceProcessorNode";
+
@NonNull
final SurfaceProcessorInternal mSurfaceProcessor;
@NonNull
@@ -153,8 +157,12 @@
@Override
public void onSuccess(@Nullable SurfaceOutput surfaceOutput) {
Preconditions.checkNotNull(surfaceOutput);
- mSurfaceProcessor.onOutputSurface(surfaceOutput);
- mSurfaceProcessor.onInputSurface(surfaceRequest);
+ try {
+ mSurfaceProcessor.onOutputSurface(surfaceOutput);
+ mSurfaceProcessor.onInputSurface(surfaceRequest);
+ } catch (ProcessingException e) {
+ Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
+ }
}
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
index 87a8644..3873031 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
@@ -18,8 +18,13 @@
import static androidx.core.util.Preconditions.checkState;
+import android.os.Build;
+
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ProcessingException;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
import androidx.camera.core.SurfaceRequest;
@@ -33,8 +38,11 @@
* makes sure that CameraX always invoke the {@link SurfaceProcessor} on the correct
* {@link Executor}.
*/
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class SurfaceProcessorWithExecutor implements SurfaceProcessorInternal {
+ private static final String TAG = "SurfaceProcessor";
+
@NonNull
private final SurfaceProcessor mSurfaceProcessor;
@NonNull
@@ -63,12 +71,24 @@
@Override
public void onInputSurface(@NonNull SurfaceRequest request) {
- mExecutor.execute(() -> mSurfaceProcessor.onInputSurface(request));
+ mExecutor.execute(() -> {
+ try {
+ mSurfaceProcessor.onInputSurface(request);
+ } catch (ProcessingException e) {
+ Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
+ }
+ });
}
@Override
public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
- mExecutor.execute(() -> mSurfaceProcessor.onOutputSurface(surfaceOutput));
+ mExecutor.execute(() -> {
+ try {
+ mSurfaceProcessor.onOutputSurface(surfaceOutput);
+ } catch (ProcessingException e) {
+ Logger.e(TAG, "Failed to setup SurfaceProcessor output.", e);
+ }
+ });
}
@Override
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CaptureProcessorPipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/CaptureProcessorPipelineTest.kt
deleted file mode 100644
index c13e598..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/CaptureProcessorPipelineTest.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.os.Build
-import android.util.Size
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.concurrent.futures.CallbackToFutureAdapter
-import com.google.common.truth.Truth
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(
- minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class CaptureProcessorPipelineTest {
- companion object {
- private val DEFAULT_SIZE = Size(640, 480)
- }
-
- @Test
- fun canCloseUnderlyingCaptureProcessors() {
- // Sets up pre-processor
- val preProcessor = Mockito.mock(
- CaptureProcessor::class.java
- )
- var preProcessorCompleter: CallbackToFutureAdapter.Completer<Void>? = null
- val preProcessorCloseFuture =
- CallbackToFutureAdapter.getFuture<Void> { completer ->
- preProcessorCompleter = completer
- "preProcessorCloseFuture"
- }
- Mockito.`when`(preProcessor.closeFuture).thenReturn(preProcessorCloseFuture)
-
- // Sets up post-processor
- val postProcessor = Mockito.mock(
- CaptureProcessor::class.java
- )
- var postProcessorCompleter: CallbackToFutureAdapter.Completer<Void>? = null
- val postProcessorCloseFuture =
- CallbackToFutureAdapter.getFuture<Void> { completer ->
- postProcessorCompleter = completer
- "postProcessorCloseFuture"
- }
- Mockito.`when`(postProcessor.closeFuture).thenReturn(postProcessorCloseFuture)
-
- val captureProcessorPipeline = CaptureProcessorPipeline(
- preProcessor,
- 2,
- postProcessor,
- Executors.newSingleThreadExecutor()
- )
-
- // Sets up the resolution to create the intermediate image reader
- captureProcessorPipeline.onResolutionUpdate(DEFAULT_SIZE)
-
- // Calls the close() function of the CaptureProcessorPipeline
- captureProcessorPipeline.close()
-
- // Verifies whether close() function of the underlying capture processors are called
- Mockito.verify(preProcessor, Mockito.times(1)).close()
- Mockito.verify(postProcessor, Mockito.times(1)).close()
-
- // Sets up the listener to monitor whether the close future is closed or not.
- val closedLatch = CountDownLatch(1)
- captureProcessorPipeline.closeFuture.addListener(
- { closedLatch.countDown() },
- CameraXExecutors.directExecutor()
- )
-
- // Checks that the close future is not completed before the underlying capture processor
- // complete their close futures
- Truth.assertThat(closedLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse()
-
- // Completes the completer of the underlying capture processors to complete their close
- // futures
- preProcessorCompleter!!.set(null)
- postProcessorCompleter!!.set(null)
-
- // Checks whether the close future of CaptureProcessorPipeline is completed after the
- // underlying capture processors complete their close futures
- Truth.assertThat(closedLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
- }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index 8bfe101..2eae4e7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -25,16 +25,17 @@
import android.os.Looper.getMainLooper
import android.util.Pair
import android.util.Rational
-import android.util.Size
import android.view.Surface
import androidx.camera.core.ImageCapture.ImageCaptureRequest
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
+import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.ImageProxyBundle
+import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.OptionsBundle
import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.TagBundle
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -51,6 +52,7 @@
import androidx.camera.testing.fakes.FakeImageInfo
import androidx.camera.testing.fakes.FakeImageProxy
import androidx.camera.testing.fakes.FakeImageReaderProxy
+import androidx.camera.testing.fakes.FakeSessionProcessor
import androidx.concurrent.futures.ResolvableFuture
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
@@ -258,21 +260,14 @@
).isFalse()
}
+ @Config(minSdk = 28)
@Test
fun extensionIsOn_pipelineDisabled() {
assertThat(
bindImageCapture(
useProcessingPipeline = true,
- captureProcessor = object : CaptureProcessor {
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {
- }
-
- override fun process(bundle: ImageProxyBundle) {
- }
-
- override fun onResolutionUpdate(size: Size) {
- }
- }
+ bufferFormat = ImageFormat.JPEG,
+ sessionProcessor = FakeSessionProcessor(null, null)
).isProcessingPipelineEnabled
).isFalse()
}
@@ -616,14 +611,13 @@
bufferFormat: Int = ImageFormat.YUV_420_888,
imageReaderProxyProvider: ImageReaderProxyProvider? = null,
useProcessingPipeline: Boolean? = null,
- captureProcessor: CaptureProcessor? = null
+ sessionProcessor: SessionProcessor? = null
): ImageCapture {
// Arrange.
val imageCapture = createImageCapture(
captureMode,
bufferFormat,
imageReaderProxyProvider,
- captureProcessor
)
if (useProcessingPipeline != null) {
imageCapture.mUseProcessingPipeline = useProcessingPipeline
@@ -636,6 +630,28 @@
)
cameraUseCaseAdapter.setViewPort(viewPort)
+ if (sessionProcessor != null) {
+ cameraUseCaseAdapter.setExtendedConfig(object : CameraConfig {
+ override fun getConfig(): androidx.camera.core.impl.Config {
+ return OptionsBundle.emptyBundle()
+ }
+
+ override fun getSessionProcessor(
+ valueIfMissing: SessionProcessor?
+ ): SessionProcessor? {
+ return sessionProcessor
+ }
+
+ override fun getSessionProcessor(): SessionProcessor {
+ return sessionProcessor
+ }
+
+ override fun getCompatibilityId(): Identifier {
+ return Identifier.create(Any())
+ }
+ })
+ }
+
cameraUseCaseAdapter.addUseCases(Collections.singleton<UseCase>(imageCapture))
return imageCapture
}
@@ -644,8 +660,7 @@
captureMode: Int = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
// Set non jpg format by default so it doesn't trigger the exif code path.
bufferFormat: Int = ImageFormat.YUV_420_888,
- imageReaderProxyProvider: ImageReaderProxyProvider? = null,
- captureProcessor: CaptureProcessor? = null
+ imageReaderProxyProvider: ImageReaderProxyProvider? = null
): ImageCapture {
val builder = ImageCapture.Builder()
.setTargetRotation(Surface.ROTATION_0)
@@ -654,11 +669,7 @@
.setCaptureOptionUnpacker { _: UseCaseConfig<*>?, _: CaptureConfig.Builder? -> }
.setSessionOptionUnpacker { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
- if (captureProcessor != null) {
- builder.setCaptureProcessor(captureProcessor)
- } else {
- builder.setBufferFormat(bufferFormat)
- }
+ builder.setBufferFormat(bufferFormat)
if (imageReaderProxyProvider != null) {
builder.setImageReaderProxyProvider(imageReaderProxyProvider)
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
deleted file mode 100644
index 184b2b8..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.content.Context
-import android.os.Build
-import android.os.Looper
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.utils.SizeUtil
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.fakes.FakeAppConfig
-import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeCameraFactory
-import androidx.test.core.app.ApplicationProvider
-import java.io.File
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-import org.junit.Assert
-
-@RunWith(RobolectricTestRunner::class)
-@Suppress("DEPRECATION")
-@DoNotInstrument
-@Config(
- minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class VideoCaptureTest {
- @Before
- fun setUp() {
- val camera = FakeCamera()
-
- val cameraFactoryProvider =
- CameraFactory.Provider { _, _, _ ->
- val cameraFactory = FakeCameraFactory()
- cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
- camera
- }
- cameraFactory
- }
- val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
- .setCameraFactoryProvider(cameraFactoryProvider)
- .build()
- val context = ApplicationProvider.getApplicationContext<Context>()
- CameraXUtil.initialize(context, cameraXConfig).get()
- }
-
- @After
- fun tearDown() {
- CameraXUtil.shutdown().get()
- }
-
- @Test
- fun startRecording_beforeUseCaseIsBound() {
- val videoCapture = VideoCapture.Builder().build()
- val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val outputFileOptions = VideoCapture.OutputFileOptions.Builder(file).build()
- val callback = mock(VideoCapture.OnVideoSavedCallback::class.java)
- videoCapture.startRecording(
- outputFileOptions,
- CameraXExecutors.mainThreadExecutor(),
- callback
- )
- shadowOf(Looper.getMainLooper()).idle()
-
- verify(callback).onError(eq(VideoCapture.ERROR_INVALID_CAMERA), anyString(), any())
- }
-
- @Test
- fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
- Assert.assertThrows(IllegalArgumentException::class.java) {
- VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
- .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
- }
- }
-
- @Test
- fun throwException_whenSetTargetResolutionWithResolutionSelector() {
- Assert.assertThrows(IllegalArgumentException::class.java) {
- VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
- .setResolutionSelector(ResolutionSelector.Builder().build())
- .build()
- }
- }
-
- @Test
- fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
- Assert.assertThrows(IllegalArgumentException::class.java) {
- VideoCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
- .setResolutionSelector(ResolutionSelector.Builder().build())
- .build()
- }
- }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
index 2f2c339..92da45c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/SurfaceSorterTest.kt
@@ -88,50 +88,6 @@
assertThat(outputConfigs6.last()).isEqualTo(videoOutput)
}
- @Suppress("DEPRECATION")
- @Test
- fun sort_previewSurfaceIsInTheFirstAndVideoCaptureSurfaceIsInTheLast() {
- // Arrange.
- val videoOutput = SessionConfig.OutputConfig.builder(
- createSurface(containerClass = androidx.camera.core.VideoCapture::class.java)).build()
- val previewOutput = SessionConfig.OutputConfig.builder(
- createSurface(containerClass = Preview::class.java)).build()
- val imageOutput = SessionConfig.OutputConfig.builder(
- createSurface(containerClass = ImageCapture::class.java)).build()
- val surfaceSorter = SurfaceSorter()
-
- // All combinations
- val outputConfigs1 = mutableListOf(previewOutput, videoOutput, imageOutput)
- val outputConfigs2 = mutableListOf(previewOutput, imageOutput, videoOutput)
- val outputConfigs3 = mutableListOf(videoOutput, previewOutput, imageOutput)
- val outputConfigs4 = mutableListOf(videoOutput, imageOutput, previewOutput)
- val outputConfigs5 = mutableListOf(imageOutput, videoOutput, previewOutput)
- val outputConfigs6 = mutableListOf(imageOutput, previewOutput, videoOutput)
-
- // Act.
- surfaceSorter.sort(outputConfigs1)
- surfaceSorter.sort(outputConfigs2)
- surfaceSorter.sort(outputConfigs3)
- surfaceSorter.sort(outputConfigs4)
- surfaceSorter.sort(outputConfigs5)
- surfaceSorter.sort(outputConfigs6)
-
- // Assert.
- assertThat(outputConfigs1.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs2.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs3.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs4.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs5.first()).isEqualTo(previewOutput)
- assertThat(outputConfigs6.first()).isEqualTo(previewOutput)
-
- assertThat(outputConfigs1.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs2.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs3.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs4.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs5.last()).isEqualTo(videoOutput)
- assertThat(outputConfigs6.last()).isEqualTo(videoOutput)
- }
-
private fun createSurface(
containerClass: Class<*>
): DeferrableSurface {
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index f78b5e8..3c1cbc4 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -40,6 +40,7 @@
testImplementation(libs.truth)
testImplementation(project(":camera:camera-testing"))
testImplementation(project(":camera:camera-extensions-stub"))
+ testImplementation(libs.testCore)
// To use the extensions-stub for testing directly.
androidTestImplementation(libs.testExtJunit)
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionTest.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionTest.java
deleted file mode 100644
index ba39474..0000000
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.extensions;
-
-import static androidx.camera.extensions.util.ExtensionsTestUtil.isTargetDeviceAvailableForExtensions;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNotNull;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Build;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
-import androidx.camera.core.impl.ImageCaptureConfig;
-import androidx.camera.core.impl.PreviewConfig;
-import androidx.camera.extensions.util.ExtensionsTestUtil;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.fakes.FakeLifecycleOwner;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.LargeTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@LargeTest
-@RunWith(Parameterized.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
-public class ExtensionTest {
-
- @Rule
- public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
- new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
- );
-
- @Parameterized.Parameters(name = "effect = {0}, facing = {1}")
- public static Collection<Object[]> getParameters() {
- return ExtensionsTestUtil.getAllExtensionsLensFacingCombinations();
- }
-
- private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
- private final Context mContext = ApplicationProvider.getApplicationContext();
-
- @ExtensionMode.Mode
- private final int mExtensionMode;
- @CameraSelector.LensFacing
- private final int mLensFacing;
-
- private ProcessCameraProvider mProcessCameraProvider = null;
- private FakeLifecycleOwner mFakeLifecycleOwner;
- private CameraSelector mBaseCameraSelector;
- private CameraSelector mExtensionsCameraSelector;
- private ExtensionsManager mExtensionsManager;
-
- public ExtensionTest(@ExtensionMode.Mode int extensionMode,
- @CameraSelector.LensFacing int lensFacing) {
- mExtensionMode = extensionMode;
- mLensFacing = lensFacing;
- }
-
- @Before
- public void setUp() throws Exception {
- assumeTrue(ExtensionsTestUtil.isTargetDeviceAvailableForExtensions(mLensFacing,
- mExtensionMode));
-
- mProcessCameraProvider = ProcessCameraProvider.getInstance(mContext).get(10000,
- TimeUnit.MILLISECONDS);
- mExtensionsManager = ExtensionsManager.getInstanceAsync(mContext,
- mProcessCameraProvider).get(10000, TimeUnit.MILLISECONDS);
- assumeTrue(isTargetDeviceAvailableForExtensions(mLensFacing, mExtensionMode));
- mBaseCameraSelector = new CameraSelector.Builder().requireLensFacing(mLensFacing).build();
- assumeTrue(mExtensionsManager.isExtensionAvailable(mBaseCameraSelector, mExtensionMode));
-
- mExtensionsCameraSelector = mExtensionsManager.getExtensionEnabledCameraSelector(
- mBaseCameraSelector, mExtensionMode);
-
- mFakeLifecycleOwner = new FakeLifecycleOwner();
- mFakeLifecycleOwner.startAndResume();
- }
-
- @After
- public void cleanUp() throws InterruptedException, ExecutionException, TimeoutException {
- if (mProcessCameraProvider != null) {
- mInstrumentation.runOnMainSync(() -> mProcessCameraProvider.unbindAll());
- mProcessCameraProvider.shutdown().get(10000, TimeUnit.MILLISECONDS);
- mExtensionsManager.shutdown().get(10000, TimeUnit.MILLISECONDS);
- }
- }
-
- @Test
- public void testEventCallbackInConfig() {
- Preview preview = new Preview.Builder().build();
- ImageCapture imageCapture = new ImageCapture.Builder().build();
-
- mInstrumentation.runOnMainSync(
- () -> mProcessCameraProvider.bindToLifecycle(mFakeLifecycleOwner,
- mExtensionsCameraSelector, preview, imageCapture));
-
- // Verify Preview config should have related callback.
- PreviewConfig previewConfig = (PreviewConfig) preview.getCurrentConfig();
- assertNotNull(previewConfig.getUseCaseEventCallback());
- CameraEventCallbacks callback1 = new Camera2ImplConfig(
- previewConfig).getCameraEventCallback(
- null);
- assertNotNull(callback1);
- assertEquals(callback1.getAllItems().size(), 1);
- assertThat(callback1.getAllItems().get(0)).isInstanceOf(CameraEventCallback.class);
-
- // Verify ImageCapture config should have related callback.
- ImageCaptureConfig imageCaptureConfig =
- (ImageCaptureConfig) imageCapture.getCurrentConfig();
- assertNotNull(imageCaptureConfig.getUseCaseEventCallback());
- assertNotNull(imageCaptureConfig.getCaptureBundle());
- CameraEventCallbacks callback2 = new Camera2ImplConfig(
- imageCaptureConfig).getCameraEventCallback(null);
- assertNotNull(callback2);
- assertEquals(callback2.getAllItems().size(), 1);
- assertThat(callback2.getAllItems().get(0)).isInstanceOf(CameraEventCallback.class);
- }
-}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index 88efac3..90e2733 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -17,11 +17,14 @@
package androidx.camera.extensions
import android.hardware.camera2.CameraCharacteristics
+import androidx.annotation.NonNull
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
+import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.extensions.internal.ExtensionVersion
import androidx.camera.extensions.internal.Version
import androidx.camera.extensions.internal.VersionName
@@ -30,6 +33,9 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.video.MediaSpec
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoOutput
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -404,7 +410,6 @@
}
}
- @Suppress("DEPRECATION")
@Test
fun throwIllegalArgumentException_whenBindingVideoCapture(): Unit = runBlocking {
val extensionCameraSelector = checkExtensionAvailabilityAndInit()
@@ -416,7 +421,7 @@
cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
extensionCameraSelector,
- androidx.camera.core.VideoCapture.Builder().build()
+ createVideoCapture()
)
}
}
@@ -458,4 +463,27 @@
characteristics
) && previewExtenderImpl.isExtensionAvailable(cameraId, characteristics)
}
+
+ private fun createVideoCapture(): VideoCapture<TestVideoOutput> {
+ val mediaSpec = MediaSpec.builder().build()
+ val videoOutput = TestVideoOutput()
+ videoOutput.mediaSpecObservable.setState(mediaSpec)
+ return VideoCapture.withOutput(videoOutput)
+ }
+
+ /** A fake implementation of VideoOutput */
+ private class TestVideoOutput : VideoOutput {
+ val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+ MutableStateObservable.withInitialState(MediaSpec.builder().build())
+ var surfaceRequest: SurfaceRequest? = null
+ var sourceState: VideoOutput.SourceState? = null
+
+ override fun onSurfaceRequested(@NonNull request: SurfaceRequest) {
+ surfaceRequest = request
+ }
+ override fun getMediaSpec() = mediaSpecObservable
+ override fun onSourceStateChanged(@NonNull sourceState: VideoOutput.SourceState) {
+ this.sourceState = sourceState
+ }
+ }
}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index acd5027..e640f36 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -201,53 +201,6 @@
verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture, imageAnalysis)
}
- @SdkSuppress(minSdkVersion = 29) // YUV to JPEG requires api level >= 29
- @Test
- fun useCasesCanWork_captureOutputFormatIsYUV() = runBlocking {
- var captureOutputSurface: Surface? = null
- var intermediaConfigId = -1
- var captureOutputSurfaceFormat = 0
- val fakeSessionProcessImpl = FakeSessionProcessImpl(
- // Directly use output surface
- previewConfigBlock = { outputSurfaceImpl ->
- Camera2OutputConfigImplBuilder
- .newSurfaceConfig(outputSurfaceImpl.surface)
- .build()
- },
- // Has intermediate image reader to process YUV.
- captureConfigBlock = { outputSurfaceImpl ->
- captureOutputSurfaceFormat = outputSurfaceImpl.imageFormat
- captureOutputSurface = outputSurfaceImpl.surface
- Camera2OutputConfigImplBuilder
- .newImageReaderConfig(outputSurfaceImpl.size, ImageFormat.YUV_420_888, 2)
- .build()
- .also {
- intermediaConfigId = it.id
- }
- },
- onCaptureSessionStarted = {
- // Emulates the processing, write an image to the output surface.
- val imageWriter = ImageWriter.newInstance(captureOutputSurface!!, 2)
- it.setImageProcessor(intermediaConfigId) { _, _, image, _ ->
- val inputImage = imageWriter.dequeueInputImage()
- imageWriter.queueInputImage(inputImage)
- image.decrement()
- }
- }
- )
-
- val preview = Preview.Builder().build()
- val imageCapture = ImageCapture.Builder()
- .setSupportedResolutions(
- listOf(
- android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480)))
- )
- )
- .build()
- verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture)
- assertThat(captureOutputSurfaceFormat).isEqualTo(ImageFormat.YUV_420_888)
- }
-
private suspend fun assumeAllowsSharedSurface() = withContext(Dispatchers.Main) {
val imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
val maxSharedSurfaceCount =
@@ -318,7 +271,7 @@
}
// Test if physicalCameraId is set and returned in the image received in the image processor.
- @SdkSuppress(minSdkVersion = 29) // YUV to JPEG requires api level >= 29
+ @SdkSuppress(minSdkVersion = 28) // physical camera id is supported in API28+
@Test
fun useCasesCanWork_setPhysicalCameraId() = runBlocking {
assumeAllowsSharedSurface()
@@ -326,9 +279,9 @@
assumeTrue(physicalCameraIdList.isNotEmpty())
val physicalCameraId = physicalCameraIdList[0]
- var captureOutputSurface: Surface? = null
- var intermediaConfigId = -1
+ var analysisOutputSurface: Surface? = null
var sharedConfigId = -1
+ var intermediaConfigId = -1
val deferredImagePhysicalCameraId = CompletableDeferred<String?>()
val deferredSharedImagePhysicalCameraId = CompletableDeferred<String?>()
@@ -340,25 +293,29 @@
.setPhysicalCameraId(physicalCameraId)
.build()
},
+ // Directly use output surface
+ captureConfigBlock = {
+ Camera2OutputConfigImplBuilder
+ .newSurfaceConfig(it.surface)
+ .build()
+ },
// Has intermediate image reader to process YUV
- captureConfigBlock = { outputSurfaceImpl ->
- captureOutputSurface = outputSurfaceImpl.surface
+ analysisConfigBlock = { outputSurfaceImpl ->
+ analysisOutputSurface = outputSurfaceImpl.surface
val sharedConfig = Camera2OutputConfigImplBuilder.newImageReaderConfig(
outputSurfaceImpl.size, outputSurfaceImpl.imageFormat, 2
- ).setPhysicalCameraId(physicalCameraId).build()
- sharedConfigId = sharedConfig.id
+ ).setPhysicalCameraId(physicalCameraId)
+ .build().also { sharedConfigId = it.id }
Camera2OutputConfigImplBuilder
.newImageReaderConfig(outputSurfaceImpl.size, ImageFormat.YUV_420_888, 2)
.setPhysicalCameraId(physicalCameraId)
.addSurfaceSharingOutputConfig(sharedConfig)
.build()
- .also {
- intermediaConfigId = it.id
- }
+ .also { intermediaConfigId = it.id }
},
onCaptureSessionStarted = { requestProcessor ->
- val imageWriter = ImageWriter.newInstance(captureOutputSurface!!, 2)
+ val imageWriter = ImageWriter.newInstance(analysisOutputSurface!!, 2)
requestProcessor.setImageProcessor(intermediaConfigId) {
_, _, image, physicalCameraIdOfImage ->
deferredImagePhysicalCameraId.complete(physicalCameraIdOfImage)
@@ -375,12 +332,9 @@
)
val preview = Preview.Builder().build()
- val imageCapture = ImageCapture.Builder()
- .setSupportedResolutions(
- listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480))))
- )
- .build()
- verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture)
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ verifyUseCasesOutput(fakeSessionProcessImpl, preview, imageCapture, imageAnalysis)
assertThat(deferredImagePhysicalCameraId.awaitWithTimeout(2000))
.isEqualTo(physicalCameraId)
assertThat(deferredSharedImagePhysicalCameraId.awaitWithTimeout(2000))
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
index 7085869..fabe251 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CaptureRequest
import android.util.Pair
import android.util.Size
import androidx.camera.camera2.Camera2Config
@@ -28,8 +27,6 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.extensions.ExtensionMode
-import androidx.camera.extensions.impl.CaptureProcessorImpl
-import androidx.camera.extensions.impl.CaptureStageImpl
import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
@@ -53,9 +50,6 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -97,64 +91,6 @@
@Test
@MediumTest
- fun extenderLifeCycleTest_noMoreInteractionsBeforeAndAfterInitDeInit(): Unit =
- runBlocking {
- val mockImageCaptureExtenderImpl = mock(ImageCaptureExtenderImpl::class.java)
- val captureStages = mutableListOf<CaptureStageImpl>()
-
- captureStages.add(FakeCaptureStage())
-
- Mockito.`when`(mockImageCaptureExtenderImpl.captureStages).thenReturn(captureStages)
- Mockito.`when`(mockImageCaptureExtenderImpl.captureProcessor).thenReturn(
- mock(CaptureProcessorImpl::class.java)
- )
- val mockVendorExtender = mock(BasicVendorExtender::class.java)
- Mockito.`when`(mockVendorExtender.imageCaptureExtenderImpl)
- .thenReturn(mockImageCaptureExtenderImpl)
-
- val imageCapture = createImageCaptureWithExtenderImpl(mockVendorExtender)
-
- // Binds the use case to trigger the camera pipeline operations
- withContext(Dispatchers.Main) {
- cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, imageCapture)
- }
-
- // To verify the event callbacks in order, and to verification of the onEnableSession()
- // is also used to wait for the capture session created. The test for the unbind
- // would come after the capture session was created.
- verify(mockImageCaptureExtenderImpl, timeout(3000)).captureProcessor
- verify(mockImageCaptureExtenderImpl, timeout(3000)).maxCaptureStage
-
- verify(mockVendorExtender, timeout(3000)).supportedCaptureOutputResolutions
-
- val inOrder = Mockito.inOrder(mockImageCaptureExtenderImpl)
- inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce()).captureStages
- inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000)).onInit(
- any(String::class.java),
- any(CameraCharacteristics::class.java),
- any(Context::class.java)
- )
- inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce())
- .onPresetSession()
- inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce())
- .onEnableSession()
-
- withContext(Dispatchers.Main) {
- // Unbind the use case to test the onDisableSession and onDeInit.
- cameraProvider.unbind(imageCapture)
- }
-
- // To verify the onDisableSession and onDeInit.
- inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000).atLeastOnce())
- .onDisableSession()
- inOrder.verify(mockImageCaptureExtenderImpl, timeout(3000)).onDeInit()
-
- // To verify there is no any other calls on the mock.
- verifyNoMoreInteractions(mockImageCaptureExtenderImpl)
- }
-
- @Test
- @MediumTest
fun canSetSupportedResolutionsToConfigTest(): Unit = runBlocking {
assumeTrue(CameraUtil.deviceHasCamera())
@@ -172,11 +108,8 @@
targetFormatResolutionsPairList
)
- val mockVendorExtender = mock(BasicVendorExtender::class.java)
- Mockito.`when`(mockVendorExtender.imageCaptureExtenderImpl)
- .thenReturn(mockImageCaptureExtenderImpl)
-
- val preview = createImageCaptureWithExtenderImpl(mockVendorExtender)
+ val vendorExtender = BasicVendorExtender(mockImageCaptureExtenderImpl, null)
+ val preview = createImageCaptureWithExtenderImpl(vendorExtender)
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
@@ -197,12 +130,19 @@
}
}
- private fun createImageCaptureWithExtenderImpl(basicVendorExtender: BasicVendorExtender) =
- ImageCapture.Builder().also {
- ImageCaptureConfigProvider(extensionMode, basicVendorExtender, context).apply {
- updateBuilderConfig(it, extensionMode, basicVendorExtender, context)
+ private suspend fun createImageCaptureWithExtenderImpl(
+ basicVendorExtender: BasicVendorExtender
+ ): ImageCapture {
+ withContext(Dispatchers.Main) {
+ val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+ basicVendorExtender.init(camera.cameraInfo)
+ }
+ return ImageCapture.Builder().also {
+ ImageCaptureConfigProvider(extensionMode, basicVendorExtender).apply {
+ updateBuilderConfig(it, extensionMode, basicVendorExtender)
}
}.build()
+ }
private fun generateImageCaptureSupportedResolutions(): List<Pair<Int, Array<Size>>> {
val formatResolutionsPairList = mutableListOf<Pair<Int, Array<Size>>>()
@@ -218,9 +158,4 @@
return formatResolutionsPairList
}
-
- private class FakeCaptureStage : CaptureStageImpl {
- override fun getId() = 0
- override fun getParameters(): List<Pair<CaptureRequest.Key<*>, Any>> = emptyList()
- }
}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
index fc1d2f0..6734ece 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
@@ -18,11 +18,7 @@
import android.content.Context
import android.graphics.ImageFormat
-import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.TotalCaptureResult
-import android.media.Image
import android.util.Pair
import android.util.Size
import androidx.camera.camera2.Camera2Config
@@ -31,15 +27,10 @@
import androidx.camera.core.Preview
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.extensions.ExtensionMode
-import androidx.camera.extensions.impl.CaptureStageImpl
import androidx.camera.extensions.impl.PreviewExtenderImpl
-import androidx.camera.extensions.impl.PreviewImageProcessorImpl
-import androidx.camera.extensions.impl.RequestUpdateProcessorImpl
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -56,14 +47,9 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -76,20 +62,6 @@
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val surfaceTextureCallback: SurfaceTextureCallback =
- object : SurfaceTextureCallback {
- override fun onSurfaceTextureReady(
- surfaceTexture: SurfaceTexture,
- resolution: Size
- ) {
- // No-op.
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- // No-op.
- }
- }
-
// Tests in this class majorly use mock objects to run the test. No matter which extension
// mode is use, it should not affect the test results.
@ExtensionMode.Mode
@@ -118,158 +90,6 @@
}
@Test
- fun extenderLifeCycleTest_noMoreInvokeBeforeAndAfterInitDeInit(): Unit = runBlocking {
- val mockPreviewExtenderImpl = mock(PreviewExtenderImpl::class.java)
-
- Mockito.`when`(mockPreviewExtenderImpl.processorType).thenReturn(
- PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR
- )
- Mockito.`when`(mockPreviewExtenderImpl.processor)
- .thenReturn(mock(PreviewImageProcessorImpl::class.java))
- Mockito.`when`(
- mockPreviewExtenderImpl.isExtensionAvailable(
- any(String::class.java),
- any(CameraCharacteristics::class.java)
- )
- ).thenReturn(true)
-
- val mockVendorExtender = mock(BasicVendorExtender::class.java)
- Mockito.`when`(mockVendorExtender.previewExtenderImpl)
- .thenReturn(mockPreviewExtenderImpl)
-
- val preview = createPreviewWithExtenderImpl(mockVendorExtender)
-
- withContext(Dispatchers.Main) {
- // To set the update listener and Preview will change to active state.
- preview.setSurfaceProvider(
- SurfaceTextureProvider.createSurfaceTextureProvider(surfaceTextureCallback)
- )
-
- cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
- }
-
- // To verify the call in order after bind to life cycle, and to verification of the
- // getCaptureStages() is also used to wait for the capture session created. The test for
- // the unbind would come after the capture session was created. Ignore any of the calls
- // unrelated to the ExtenderStateListener.
- verify(mockPreviewExtenderImpl, timeout(3000)).processorType
- verify(mockPreviewExtenderImpl, timeout(3000)).processor
-
- verify(mockVendorExtender, timeout(3000)).supportedPreviewOutputResolutions
-
- val inOrder = Mockito.inOrder(*Mockito.ignoreStubs(mockPreviewExtenderImpl))
- inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onInit(
- any(String::class.java),
- any(CameraCharacteristics::class.java),
- any(Context::class.java)
- )
-
- inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onPresetSession()
- inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onEnableSession()
- inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).captureStage
-
- withContext(Dispatchers.Main) {
- // Unbind the use case to test the onDisableSession and onDeInit.
- cameraProvider.unbind(preview)
- }
-
- // To verify the onDisableSession and onDeInit.
- inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onDisableSession()
- inOrder.verify(mockPreviewExtenderImpl, timeout(3000)).onDeInit()
-
- // To verify there is no any other calls on the mock.
- verifyNoMoreInteractions(mockPreviewExtenderImpl)
- }
-
- @Test
- fun getCaptureStagesTest_shouldSetToRepeatingRequest(): Unit = runBlocking {
- // Set up a result for getCaptureStages() testing.
- val fakeCaptureStageImpl: CaptureStageImpl = FakeCaptureStageImpl()
- val mockPreviewExtenderImpl = mock(PreviewExtenderImpl::class.java)
- val mockRequestUpdateProcessorImpl = mock(RequestUpdateProcessorImpl::class.java)
-
- // The mock an RequestUpdateProcessorImpl to capture the returned TotalCaptureResult
- Mockito.`when`(mockPreviewExtenderImpl.processorType).thenReturn(
- PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY
- )
- Mockito.`when`(mockPreviewExtenderImpl.processor).thenReturn(mockRequestUpdateProcessorImpl)
- Mockito.`when`(
- mockPreviewExtenderImpl.isExtensionAvailable(
- any(String::class.java),
- any(CameraCharacteristics::class.java)
- )
- ).thenReturn(true)
- Mockito.`when`(mockPreviewExtenderImpl.captureStage).thenReturn(fakeCaptureStageImpl)
-
- val mockVendorExtender = mock(BasicVendorExtender::class.java)
- Mockito.`when`(mockVendorExtender.previewExtenderImpl)
- .thenReturn(mockPreviewExtenderImpl)
- val preview = createPreviewWithExtenderImpl(mockVendorExtender)
-
- withContext(Dispatchers.Main) {
- // To set the update listener and Preview will change to active state.
- preview.setSurfaceProvider(
- SurfaceTextureProvider.createSurfaceTextureProvider(surfaceTextureCallback)
- )
-
- cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
- }
-
- val captureResultArgumentCaptor = ArgumentCaptor.forClass(
- TotalCaptureResult::class.java
- )
- verify(mockRequestUpdateProcessorImpl, timeout(3000).atLeastOnce()).process(
- captureResultArgumentCaptor.capture()
- )
-
- // TotalCaptureResult might be captured multiple times. Only care to get one instance of
- // it, since they should all have the same value for the tested key
- val totalCaptureResult = captureResultArgumentCaptor.value
-
- // To verify the capture result should include the parameter of the getCaptureStages().
- val parameters = fakeCaptureStageImpl.parameters
- for (parameter: Pair<CaptureRequest.Key<*>?, Any> in parameters) {
- assertThat(totalCaptureResult.request[parameter.first] == parameter.second)
- }
- }
-
- @Test
- fun processShouldBeInvoked_typeImageProcessor(): Unit = runBlocking {
- // The type image processor will invoke PreviewImageProcessor.process()
- val mockPreviewImageProcessorImpl = mock(PreviewImageProcessorImpl::class.java)
- val mockPreviewExtenderImpl = mock(PreviewExtenderImpl::class.java)
-
- Mockito.`when`(mockPreviewExtenderImpl.processor).thenReturn(mockPreviewImageProcessorImpl)
- Mockito.`when`(mockPreviewExtenderImpl.processorType)
- .thenReturn(PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR)
- Mockito.`when`(
- mockPreviewExtenderImpl.isExtensionAvailable(
- any(String::class.java),
- any(CameraCharacteristics::class.java)
- )
- ).thenReturn(true)
-
- val mockVendorExtender = mock(BasicVendorExtender::class.java)
- Mockito.`when`(mockVendorExtender.previewExtenderImpl)
- .thenReturn(mockPreviewExtenderImpl)
-
- val preview = createPreviewWithExtenderImpl(mockVendorExtender)
-
- withContext(Dispatchers.Main) {
- // To set the update listener and Preview will change to active state.
- preview.setSurfaceProvider(
- SurfaceTextureProvider.createSurfaceTextureProvider(surfaceTextureCallback)
- )
-
- cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, preview)
- }
-
- // To verify the process() method was invoked with non-null TotalCaptureResult input.
- verify(mockPreviewImageProcessorImpl, Mockito.timeout(3000).atLeastOnce())
- .process(any(Image::class.java), ArgumentMatchers.any(TotalCaptureResult::class.java))
- }
-
- @Test
@MediumTest
fun canSetSupportedResolutionsToConfigTest(): Unit = runBlocking {
assumeTrue(CameraUtil.deviceHasCamera())
@@ -290,9 +110,7 @@
targetFormatResolutionsPairList
)
- val mockVendorExtender = mock(BasicVendorExtender::class.java)
- Mockito.`when`(mockVendorExtender.previewExtenderImpl)
- .thenReturn(mockPreviewExtenderImpl)
+ val mockVendorExtender = BasicVendorExtender(null, mockPreviewExtenderImpl)
val preview = createPreviewWithExtenderImpl(mockVendorExtender)
@@ -315,12 +133,19 @@
}
}
- private fun createPreviewWithExtenderImpl(basicVendorExtender: BasicVendorExtender) =
- Preview.Builder().also {
- PreviewConfigProvider(extensionMode, basicVendorExtender, context).apply {
- updateBuilderConfig(it, extensionMode, basicVendorExtender, context)
+ private suspend fun createPreviewWithExtenderImpl(
+ basicVendorExtender: BasicVendorExtender
+ ): Preview {
+ withContext(Dispatchers.Main) {
+ val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+ basicVendorExtender.init(camera.cameraInfo)
+ }
+ return Preview.Builder().also {
+ PreviewConfigProvider(extensionMode, basicVendorExtender).apply {
+ updateBuilderConfig(it, extensionMode, basicVendorExtender)
}
}.build()
+ }
private fun generatePreviewSupportedResolutions(): List<Pair<Int, Array<Size>>> {
val formatResolutionsPairList = mutableListOf<Pair<Int, Array<Size>>>()
@@ -336,14 +161,4 @@
return formatResolutionsPairList
}
-
- private class FakeCaptureStageImpl : CaptureStageImpl {
- override fun getId() = 0
- override fun getParameters(): List<Pair<CaptureRequest.Key<*>, Any>> = mutableListOf(
- Pair.create(
- CaptureRequest.CONTROL_EFFECT_MODE,
- CaptureRequest.CONTROL_EFFECT_MODE_SEPIA
- )
- )
- }
}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
new file mode 100644
index 0000000..3c3aa9d
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -0,0 +1,991 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageWriter
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Pair
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraFilter
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraState
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.core.impl.CameraConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
+import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.OutputSurface
+import androidx.camera.core.impl.RequestProcessor
+import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.extensions.impl.CaptureProcessorImpl
+import androidx.camera.extensions.impl.CaptureStageImpl
+import androidx.camera.extensions.impl.ExtenderStateListener
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl
+import androidx.camera.extensions.impl.PreviewExtenderImpl
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_NONE
+import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.lifecycle.Observer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(Parameterized::class)
+class BasicExtenderSessionProcessorTest(
+ private val hasCaptureProcessor: Boolean,
+ private val previewProcessorType: ProcessorType
+) {
+ companion object {
+ @Parameterized.Parameters(name = "hasCaptureProcessor = {0}, previewProcessorType = {1}")
+ @JvmStatic
+ fun parameters() = listOf(
+ arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_NONE),
+ arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_NONE),
+ arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_REQUEST_UPDATE_ONLY),
+ arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_REQUEST_UPDATE_ONLY),
+ arrayOf(false /* No CaptureProcessor */, PROCESSOR_TYPE_IMAGE_PROCESSOR),
+ arrayOf(true /* Has CaptureProcessor */, PROCESSOR_TYPE_IMAGE_PROCESSOR)
+ )
+
+ private fun createCaptureStage(
+ id: Int = 0,
+ parameters: List<Pair<CaptureRequest.Key<*>, Any>> = mutableListOf()
+ ): CaptureStageImpl {
+ return object : CaptureStageImpl {
+ override fun getId() = id
+ override fun getParameters() = parameters
+ }
+ }
+ }
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+ )
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+ private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+ private lateinit var fakePreviewExtenderImpl: FakePreviewExtenderImpl
+ private lateinit var fakeCaptureExtenderImpl: FakeImageCaptureExtenderImpl
+ private lateinit var basicExtenderSessionProcessor: BasicExtenderSessionProcessor
+
+ @Before
+ fun setUp() = runBlocking {
+ cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+ withContext(Dispatchers.Main) {
+ fakeLifecycleOwner = FakeLifecycleOwner()
+ fakeLifecycleOwner.startAndResume()
+ }
+
+ if (hasCaptureProcessor || previewProcessorType == PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+ assumeTrue(areYuvYuvYuvAndYuvYuvPrivateSupported())
+ }
+
+ fakePreviewExtenderImpl = FakePreviewExtenderImpl(previewProcessorType)
+ fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(hasCaptureProcessor)
+ basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+ fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+ )
+ }
+
+ private suspend fun areYuvYuvYuvAndYuvYuvPrivateSupported(): Boolean {
+ if (Build.BRAND.equals("SAMSUNG", ignoreCase = true)) {
+ return true
+ }
+ val camera = withContext(Dispatchers.Main) {
+ cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+ }
+ val hardwareLevel = Camera2CameraInfo.from(camera.cameraInfo).getCameraCharacteristic(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+ )
+
+ return hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3 ||
+ hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ }
+
+ @After
+ fun tearDown() = runBlocking {
+ if (::cameraProvider.isInitialized) {
+ withContext(Dispatchers.Main) {
+ cameraProvider.unbindAll()
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+ }
+ }
+ }
+
+ @Test
+ fun canOutputCorrectly(): Unit = runBlocking {
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ val previewSemaphore = Semaphore(0)
+ val analysisSemaphore = Semaphore(0)
+ verifyUseCasesOutput(
+ preview,
+ imageCapture,
+ imageAnalysis,
+ previewSemaphore,
+ analysisSemaphore
+ )
+ }
+
+ @Test
+ fun imageCaptureError(): Unit = runBlocking {
+ assumeTrue(hasCaptureProcessor)
+ fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(
+ hasCaptureProcessor, throwErrorOnProcess = true
+ )
+ basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+ fakePreviewExtenderImpl, fakeCaptureExtenderImpl, context
+ )
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ assertThrows<ImageCaptureException> {
+ verifyUseCasesOutput(preview, imageCapture, imageAnalysis)
+ }
+ }
+
+ @Test
+ fun canOutputCorrectly_withoutAnalysis(): Unit = runBlocking {
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder().build()
+ val previewSemaphore = Semaphore(0)
+ verifyUseCasesOutput(
+ preview = preview,
+ imageCapture = imageCapture,
+ previewFrameSemaphore = previewSemaphore
+ )
+ }
+
+ suspend fun getSensorRotationDegrees(rotation: Int): Int {
+ return withContext(Dispatchers.Main) {
+ val camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector)
+ camera.cameraInfo.getSensorRotationDegrees(rotation)
+ }
+ }
+
+ @Test
+ fun canOutputCorrectly_setTargetRotation(): Unit = runBlocking {
+ assumeTrue(hasCaptureProcessor)
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder()
+ .setTargetRotation(Surface.ROTATION_0)
+ .build()
+ val previewSemaphore = Semaphore(0)
+ verifyUseCasesOutput(
+ preview = preview,
+ imageCapture = imageCapture,
+ previewFrameSemaphore = previewSemaphore,
+ expectedExifRotation = getSensorRotationDegrees(Surface.ROTATION_0)
+ )
+ }
+
+ @Test
+ fun canOutputCorrectlyAfterStopStart(): Unit = runBlocking {
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ val previewSemaphore = Semaphore(0)
+ val analysisSemaphore = Semaphore(0)
+
+ verifyUseCasesOutput(
+ preview,
+ imageCapture,
+ imageAnalysis,
+ previewSemaphore,
+ analysisSemaphore
+ )
+
+ fakeLifecycleOwner.pauseAndStop()
+
+ delay(1000)
+ previewSemaphore.drainPermits()
+ analysisSemaphore.drainPermits()
+ fakeLifecycleOwner.startAndResume()
+
+ assertThat(previewSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+
+ imageAnalysis.let {
+ assertThat(analysisSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ }
+
+ verifyStillCapture(imageCapture)
+ }
+
+ @Test
+ fun canInvokeEventsInOrder(): Unit = runBlocking {
+ val preview = Preview.Builder().build()
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ val previewSemaphore = Semaphore(0)
+ val analysisSemaphore = Semaphore(0)
+ val camera = verifyUseCasesOutput(
+ preview,
+ imageCapture,
+ imageAnalysis,
+ previewSemaphore,
+ analysisSemaphore
+ )
+
+ val cameraClosedLatch = CountDownLatch(1)
+ withContext(Dispatchers.Main) {
+ camera.cameraInfo.cameraState.observeForever(object : Observer<CameraState?> {
+ override fun onChanged(cameraState: CameraState?) {
+ if (cameraState?.type == CameraState.Type.CLOSED) {
+ cameraClosedLatch.countDown()
+ camera.cameraInfo.cameraState.removeObserver(this)
+ }
+ }
+ })
+ }
+
+ fakeLifecycleOwner.pauseAndStop()
+ assertThat(cameraClosedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ fakeCaptureExtenderImpl.assertInvokeOrder(listOf(
+ "onInit",
+ "onPresetSession",
+ "onEnableSession",
+ "onDisableSession",
+ "onDeInit",
+ ))
+
+ fakePreviewExtenderImpl.assertInvokeOrder(listOf(
+ "onInit",
+ "onPresetSession",
+ "onEnableSession",
+ "onDisableSession",
+ "onDeInit",
+ ))
+ }
+
+ class ResultMonitor {
+ private var latch: CountDownLatch? = null
+ private var keyToCheck: CaptureRequest.Key<*>? = null
+ private var valueToCheck: Any? = null
+
+ fun onCaptureRequestReceived(captureRequest: CaptureRequest) {
+ if (latch != null) {
+ keyToCheck?.let {
+ if (captureRequest.get(keyToCheck) == valueToCheck) {
+ latch!!.countDown()
+ }
+ }
+ }
+ }
+ fun assertCaptureKey(key: CaptureRequest.Key<*>, value: Any) {
+ keyToCheck = key
+ valueToCheck = value
+ latch = CountDownLatch(1)
+ assertThat(latch!!.await(3, TimeUnit.SECONDS)).isTrue()
+ }
+ }
+
+ @Test
+ fun repeatingRequest_containsPreviewCaptureStagesParameters(): Unit = runBlocking {
+ val previewBuilder = Preview.Builder()
+ val resultMonitor = ResultMonitor()
+ Camera2Interop.Extender(previewBuilder)
+ .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ resultMonitor.onCaptureRequestReceived(request)
+ }
+ })
+ val preview = previewBuilder.build()
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ val previewSemaphore = Semaphore(0)
+ val analysisSemaphore = Semaphore(0)
+ fakePreviewExtenderImpl.captureStage = createCaptureStage(
+ parameters = listOf(Pair(
+ CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF
+ ))
+ )
+
+ verifyUseCasesOutput(
+ preview,
+ imageCapture,
+ imageAnalysis,
+ previewSemaphore,
+ analysisSemaphore
+ )
+
+ resultMonitor.assertCaptureKey(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_OFF
+ )
+ }
+
+ @Test
+ fun processorRequestUpdateOnly_canUpdateRepeating(): Unit = runBlocking {
+ assumeTrue(previewProcessorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)
+ val previewBuilder = Preview.Builder()
+ val resultMonitor = ResultMonitor()
+ Camera2Interop.Extender(previewBuilder)
+ .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ resultMonitor.onCaptureRequestReceived(request)
+ }
+ })
+ val preview = previewBuilder.build()
+ val imageCapture = ImageCapture.Builder().build()
+ val imageAnalysis = ImageAnalysis.Builder().build()
+ val previewSemaphore = Semaphore(0)
+ val analysisSemaphore = Semaphore(0)
+ verifyUseCasesOutput(
+ preview,
+ imageCapture,
+ imageAnalysis,
+ previewSemaphore,
+ analysisSemaphore
+ )
+
+ // Trigger RequestUpdateProcessor to update repeating request to have new parameters.
+ fakePreviewExtenderImpl.fakeRequestUpdateProcessor?.captureStage =
+ createCaptureStage(
+ parameters = listOf(Pair(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_OFF)
+ )
+ )
+
+ resultMonitor.assertCaptureKey(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_OFF
+ )
+ }
+
+ @Test
+ fun imageCapture_captureRequestParametersAreCorrect(): Unit = runBlocking {
+ initBasicExtenderSessionProcessor().use {
+ fakeCaptureExtenderImpl.captureStages = listOf(
+ createCaptureStage(
+ 0, listOf(
+ Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)
+ )
+ ),
+ createCaptureStage(
+ 1, listOf(
+ Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
+ )
+ ),
+ )
+ fakePreviewExtenderImpl.captureStage = createCaptureStage(
+ 0, listOf(
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+ )
+ )
+
+ val fakeRequestProcessor = FakeRequestProcessor()
+ basicExtenderSessionProcessor.onCaptureSessionStart(fakeRequestProcessor)
+
+ basicExtenderSessionProcessor.startRepeating(object :
+ SessionProcessor.CaptureCallback {})
+ basicExtenderSessionProcessor.startCapture(object : SessionProcessor.CaptureCallback {})
+
+ val submittedRequests = withTimeout(2000) {
+ fakeRequestProcessor.awaitRequestSubmitted()
+ }
+ assertThat(submittedRequests.size).isEqualTo(2)
+ val submittedRequestParameter0 = submittedRequests[0].toParametersList()
+ val submittedRequestParameter1 = submittedRequests[1].toParametersList()
+
+ // Capture request parameters should include both Image capture capture stage and
+ // preview capture stage.
+ assertThat(submittedRequestParameter0).containsExactly(
+ // Set by image capture CaptureStageImpl "0"
+ Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF),
+ // Set by preview getCaptureStage()
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+ )
+
+ assertThat(submittedRequestParameter1).containsExactly(
+ // Set by image capture CaptureStageImpl "1"
+ Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF),
+ // Set by preview getCaptureStage()
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+ )
+ }
+ }
+
+ @Test
+ fun onEnableDisableRequestsAreSent(): Unit = runBlocking {
+ initBasicExtenderSessionProcessor().use {
+ // Verify onEnableSession
+ fakePreviewExtenderImpl.onEnableSessionCaptureStage = createCaptureStage(
+ 0, listOf(
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+ )
+ )
+
+ fakeCaptureExtenderImpl.onEnableSessionCaptureStage = createCaptureStage(
+ 0, listOf(
+ Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
+ )
+ )
+
+ val fakeRequestProcessor = FakeRequestProcessor()
+ basicExtenderSessionProcessor.onCaptureSessionStart(fakeRequestProcessor)
+
+ val onEnableSessionRequest = withTimeout(2000) {
+ fakeRequestProcessor.awaitRequestSubmitted()
+ }
+ assertThat(onEnableSessionRequest.size).isEqualTo(2)
+ assertThat(onEnableSessionRequest[0].toParametersList()).containsExactly(
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF),
+ )
+ assertThat(onEnableSessionRequest[1].toParametersList()).containsExactly(
+ Pair(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF),
+ )
+
+ // Verify onDisableSession
+ fakePreviewExtenderImpl.onDisableSessionCaptureStage = createCaptureStage(
+ 0, listOf(
+ Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF)
+ )
+ )
+ fakeCaptureExtenderImpl.onDisableSessionCaptureStage = createCaptureStage(
+ 0, listOf(
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF)
+ )
+ )
+ basicExtenderSessionProcessor.onCaptureSessionEnd()
+ val onDisableSessionRequest = withTimeout(2000) {
+ fakeRequestProcessor.awaitRequestSubmitted()
+ }
+ assertThat(onDisableSessionRequest.size).isEqualTo(2)
+ assertThat(onDisableSessionRequest[0].toParametersList()).containsExactly(
+ Pair(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF),
+ )
+ assertThat(onDisableSessionRequest[1].toParametersList()).containsExactly(
+ Pair(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF),
+ )
+ }
+ }
+
+ private fun initBasicExtenderSessionProcessor(): AutoCloseable {
+ val width = 640
+ val height = 480
+ val maxImages = 2
+ val cameraInfo = cameraProvider.availableCameraInfos[0]
+ val handlerThread = HandlerThread("CameraX-AutoDrainThread")
+ handlerThread.start()
+ val handler = Handler(handlerThread.looper)
+ val surfaceTextureHolder = SurfaceTextureProvider.createAutoDrainingSurfaceTexture(
+ CameraXExecutors.newHandlerExecutor(handler), width, height
+ ) {}
+ val previewOutputSurface = OutputSurface.create(
+ Surface(surfaceTextureHolder.surfaceTexture),
+ Size(width, height),
+ ImageFormat.PRIVATE
+ )
+ val jpegImageReader =
+ ImageReaderProxys.createIsolatedReader(width, height, ImageFormat.JPEG, maxImages)
+ val captureOutputSurface = OutputSurface.create(
+ jpegImageReader.surface!!,
+ Size(width, height),
+ ImageFormat.JPEG
+ )
+
+ basicExtenderSessionProcessor.initSession(
+ cameraInfo,
+ previewOutputSurface,
+ captureOutputSurface,
+ null
+ )
+
+ return AutoCloseable {
+ jpegImageReader.close()
+ surfaceTextureHolder.close()
+ handlerThread.quitSafely()
+ }
+ }
+
+ private fun RequestProcessor.Request.toParametersList():
+ List<Pair<CaptureRequest.Key<Any>?, Any?>> {
+ val submittedRequestParameter1 = parameters.listOptions().map {
+ @Suppress("UNCHECKED_CAST")
+ val key = it.token as CaptureRequest.Key<Any>?
+ val value = parameters.retrieveOption(it)
+ Pair(key, value)
+ }
+ return submittedRequestParameter1
+ }
+
+ /**
+ * Verify if the given use cases have expected output.
+ * 1) Preview frame is received
+ * 2) imageCapture gets a captured JPEG image
+ * 3) imageAnalysis gets a Image in Analyzer.
+ */
+ private suspend fun verifyUseCasesOutput(
+ preview: Preview,
+ imageCapture: ImageCapture,
+ imageAnalysis: ImageAnalysis? = null,
+ previewFrameSemaphore: Semaphore? = null,
+ analysisSemaphore: Semaphore? = null,
+ expectedExifRotation: Int = 0,
+ ): Camera {
+ val camera =
+ withContext(Dispatchers.Main) {
+ preview.setSurfaceProvider(
+ SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider {
+ if (previewFrameSemaphore?.availablePermits() == 0) {
+ previewFrameSemaphore.release()
+ }
+ }
+ )
+ imageAnalysis?.setAnalyzer(CameraXExecutors.mainThreadExecutor()) {
+ it.close()
+ if (analysisSemaphore?.availablePermits() == 0) {
+ analysisSemaphore.release()
+ }
+ }
+ val cameraSelector =
+ getCameraSelectorWithSessionProcessor(
+ cameraSelector,
+ basicExtenderSessionProcessor
+ )
+
+ val useCaseGroupBuilder = UseCaseGroup.Builder()
+ useCaseGroupBuilder.addUseCase(preview)
+ useCaseGroupBuilder.addUseCase(imageCapture)
+ imageAnalysis?.let { useCaseGroupBuilder.addUseCase(it) }
+
+ cameraProvider.bindToLifecycle(
+ fakeLifecycleOwner,
+ cameraSelector,
+ useCaseGroupBuilder.build()
+ )
+ }
+
+ previewFrameSemaphore?.let {
+ assertThat(it.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ }
+
+ analysisSemaphore?.let {
+ assertThat(analysisSemaphore.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ }
+
+ verifyStillCapture(imageCapture, expectedExifRotation)
+ return camera
+ }
+
+ private suspend fun verifyStillCapture(
+ imageCapture: ImageCapture,
+ expectExifRotation: Int = 0
+ ) {
+ val deferCapturedImage = CompletableDeferred<ImageProxy>()
+ imageCapture.takePicture(
+ CameraXExecutors.mainThreadExecutor(),
+ object : ImageCapture.OnImageCapturedCallback() {
+ override fun onCaptureSuccess(image: ImageProxy) {
+ deferCapturedImage.complete(image)
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ deferCapturedImage.completeExceptionally(exception)
+ }
+ })
+ withTimeout(6000) {
+ deferCapturedImage.await().use {
+ assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+ if (expectExifRotation != 0) {
+ val exif = Exif.createFromImageProxy(it)
+ assertThat(exif.rotation).isEqualTo(expectExifRotation)
+ }
+ }
+ }
+ }
+
+ private fun getCameraSelectorWithSessionProcessor(
+ cameraSelector: CameraSelector,
+ sessionProcessor: SessionProcessor
+ ): CameraSelector {
+ val identifier = Identifier.create("idStr")
+ ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
+ object : CameraConfig {
+ override fun getConfig(): Config {
+ return MutableOptionsBundle.create()
+ }
+
+ override fun getCompatibilityId(): Identifier {
+ return Identifier.create(0)
+ }
+
+ override fun getSessionProcessor(
+ valueIfMissing: SessionProcessor?
+ ): SessionProcessor {
+ return sessionProcessor
+ }
+
+ override fun getSessionProcessor(): SessionProcessor {
+ return sessionProcessor
+ }
+ }
+ }
+ val builder = CameraSelector.Builder.fromSelector(cameraSelector)
+ builder.addCameraFilter(object : CameraFilter {
+ override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
+ val newCameraInfos = mutableListOf<CameraInfo>()
+ newCameraInfos.addAll(cameraInfos)
+ return newCameraInfos
+ }
+
+ override fun getIdentifier(): Identifier {
+ return identifier
+ }
+ })
+ return builder.build()
+ }
+
+ open class FakeExtenderStateListener : ExtenderStateListener {
+ private val invokeList = mutableListOf<String>()
+ fun recordInvoking(action: String) {
+ invokeList.add(action)
+ }
+
+ fun assertInvokeOrder(expectList: List<String>) {
+ assertThat(expectList).containsExactlyElementsIn(invokeList).inOrder()
+ }
+ override fun onInit(
+ cameraId: String,
+ cameraCharacteristics: CameraCharacteristics,
+ context: Context
+ ) {
+ recordInvoking("onInit")
+ }
+
+ override fun onDeInit() {
+ recordInvoking("onDeInit")
+ }
+ override fun onPresetSession(): CaptureStageImpl? {
+ recordInvoking("onPresetSession")
+ return null
+ }
+ override fun onEnableSession(): CaptureStageImpl? {
+ recordInvoking("onEnableSession")
+ return onEnableSessionCaptureStage
+ }
+ override fun onDisableSession(): CaptureStageImpl? {
+ recordInvoking("onDisableSession")
+ return onDisableSessionCaptureStage
+ }
+
+ var onEnableSessionCaptureStage: CaptureStageImpl? = null
+ var onDisableSessionCaptureStage: CaptureStageImpl? = null
+ }
+
+ private class FakePreviewExtenderImpl(
+ private var processorType: ProcessorType = PROCESSOR_TYPE_NONE
+ ) : PreviewExtenderImpl, FakeExtenderStateListener() {
+ var fakePreviewImageProcessorImpl: FakePreviewImageProcessorImpl? = null
+ var fakeRequestUpdateProcessor: FakeRequestUpdateProcessor? = null
+
+ init {
+ when (processorType) {
+ PROCESSOR_TYPE_REQUEST_UPDATE_ONLY ->
+ fakeRequestUpdateProcessor = FakeRequestUpdateProcessor()
+ PROCESSOR_TYPE_IMAGE_PROCESSOR ->
+ fakePreviewImageProcessorImpl = FakePreviewImageProcessorImpl()
+ PROCESSOR_TYPE_NONE -> {}
+ }
+ }
+ override fun isExtensionAvailable(
+ cameraId: String,
+ cameraCharacteristics: CameraCharacteristics
+ ): Boolean {
+ return true
+ }
+ override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {
+ recordInvoking("init")
+ }
+
+ private var _captureStage: CaptureStageImpl? = null
+ override fun getCaptureStage(): CaptureStageImpl {
+ // Return CaptureStage if it is set already.
+ if (_captureStage != null) {
+ return _captureStage!!
+ }
+
+ // For PROCESSOR_TYPE_REQUEST_UPDATE_ONLY, getCaptureStage() should be in sync with
+ // RequestUpdateProcessor.
+ if (processorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
+ return fakeRequestUpdateProcessor!!.captureStage
+ }
+ return createCaptureStage()
+ }
+
+ fun setCaptureStage(captureStage: CaptureStageImpl) {
+ _captureStage = captureStage
+ }
+ override fun getProcessorType() = processorType
+ override fun getProcessor() =
+ when (processorType) {
+ PROCESSOR_TYPE_NONE -> null
+ PROCESSOR_TYPE_REQUEST_UPDATE_ONLY -> fakeRequestUpdateProcessor
+ PROCESSOR_TYPE_IMAGE_PROCESSOR -> fakePreviewImageProcessorImpl
+ }
+
+ override fun getSupportedResolutions() = null
+ override fun onDeInit() {
+ recordInvoking("onDeInit")
+ fakePreviewImageProcessorImpl?.close()
+ }
+ }
+
+ private class FakeImageCaptureExtenderImpl(
+ private val hasCaptureProcessor: Boolean = false,
+ private val throwErrorOnProcess: Boolean = false
+ ) : ImageCaptureExtenderImpl, FakeExtenderStateListener() {
+ val fakeCaptureProcessorImpl: FakeCaptureProcessorImpl? by lazy {
+ if (hasCaptureProcessor) {
+ FakeCaptureProcessorImpl(throwErrorOnProcess)
+ } else {
+ null
+ }
+ }
+
+ override fun isExtensionAvailable(
+ cameraId: String,
+ cameraCharacteristics: CameraCharacteristics
+ ): Boolean {
+ return true
+ }
+
+ override fun init(cameraId: String, cameraCharacteristics: CameraCharacteristics) {
+ recordInvoking("init")
+ }
+ override fun getCaptureProcessor() = fakeCaptureProcessorImpl
+
+ override fun getCaptureStages() = _captureStages
+ private var _captureStages: List<CaptureStageImpl> = listOf(createCaptureStage())
+ fun setCaptureStages(captureStages: List<CaptureStageImpl>) {
+ _captureStages = captureStages
+ }
+
+ override fun getMaxCaptureStage(): Int {
+ return 2
+ }
+
+ override fun getSupportedResolutions() = null
+ override fun getEstimatedCaptureLatencyRange(size: Size?) = null
+ override fun getAvailableCaptureRequestKeys(): MutableList<CaptureRequest.Key<Any>> {
+ return mutableListOf()
+ }
+
+ override fun getAvailableCaptureResultKeys(): MutableList<CaptureResult.Key<Any>> {
+ return mutableListOf()
+ }
+
+ override fun onDeInit() {
+ fakeCaptureProcessorImpl?.close()
+ recordInvoking("onDeInit")
+ }
+ }
+
+ private class FakeCaptureProcessorImpl(
+ val throwErrorOnProcess: Boolean = false
+ ) : CaptureProcessorImpl {
+ private var imageWriter: ImageWriter? = null
+ override fun process(results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?) {
+ if (throwErrorOnProcess) {
+ throw RuntimeException("Process failed")
+ }
+ val image = imageWriter!!.dequeueInputImage()
+ imageWriter!!.queueInputImage(image)
+ }
+
+ override fun process(
+ results: MutableMap<Int, Pair<Image, TotalCaptureResult>>?,
+ resultCallback: ProcessResultImpl?,
+ executor: Executor?
+ ) {
+ process(results)
+ }
+
+ override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+ imageWriter = ImageWriter.newInstance(surface, 2)
+ }
+
+ override fun onResolutionUpdate(size: Size) {}
+ override fun onImageFormatUpdate(imageFormat: Int) {}
+ fun close() {
+ imageWriter?.close()
+ imageWriter = null
+ }
+ }
+
+ private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
+ private var imageWriter: ImageWriter? = null
+ override fun process(image: Image?, result: TotalCaptureResult?) {
+ val emptyImage = imageWriter!!.dequeueInputImage()
+ imageWriter!!.queueInputImage(emptyImage)
+ }
+
+ override fun process(
+ image: Image?,
+ result: TotalCaptureResult?,
+ resultCallback: ProcessResultImpl?,
+ executor: Executor?
+ ) {
+ process(image, result)
+ }
+
+ override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+ imageWriter = ImageWriter.newInstance(surface, 2)
+ }
+
+ override fun onResolutionUpdate(size: Size) {}
+ override fun onImageFormatUpdate(imageFormat: Int) {}
+ fun close() {
+ imageWriter?.close()
+ imageWriter = null
+ }
+ }
+
+ private class FakeRequestUpdateProcessor : RequestUpdateProcessorImpl {
+ override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+ throw RuntimeException("Should not invoke this")
+ }
+
+ override fun onResolutionUpdate(size: Size) {
+ throw RuntimeException("Should not invoke this")
+ }
+
+ override fun onImageFormatUpdate(imageFormat: Int) {
+ throw RuntimeException("Should not invoke this")
+ }
+
+ override fun process(result: TotalCaptureResult?): CaptureStageImpl? {
+ return if (hasCaptureStageChange) {
+ hasCaptureStageChange = false
+ captureStage // return non-null result to trigger a repeating request update.
+ } else {
+ null
+ }
+ }
+
+ var hasCaptureStageChange = false
+ var captureStage: CaptureStageImpl = createCaptureStage()
+ set(value) {
+ hasCaptureStageChange = true
+ field = value
+ }
+ }
+
+ class FakeRequestProcessor : RequestProcessor {
+ private var deferredSubmit = CompletableDeferred<List<RequestProcessor.Request>>()
+
+ suspend fun awaitRequestSubmitted(): List<RequestProcessor.Request> {
+ return deferredSubmit.await().also {
+ // renew another deferred
+ deferredSubmit = CompletableDeferred<List<RequestProcessor.Request>>()
+ }
+ }
+ override fun submit(
+ request: RequestProcessor.Request,
+ callback: RequestProcessor.Callback
+ ): Int {
+ return submit(mutableListOf(request), callback)
+ }
+
+ override fun submit(
+ requests: MutableList<RequestProcessor.Request>,
+ callback: RequestProcessor.Callback
+ ): Int {
+ deferredSubmit.complete(requests)
+ return 0
+ }
+
+ override fun setRepeating(
+ request: RequestProcessor.Request,
+ callback: RequestProcessor.Callback
+ ): Int {
+ return 0
+ }
+
+ override fun abortCaptures() {
+ }
+
+ override fun stopRepeating() {
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
new file mode 100644
index 0000000..2ae872f
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessorTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageReader
+import android.media.ImageWriter
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraXThreads
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.testing.Camera2Util
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import java.util.concurrent.Executor
+import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(AndroidJUnit4::class)
+class PreviewProcessorTest {
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+ )
+
+ private lateinit var surfaceTexture: SurfaceTexture
+ private lateinit var previewSurface: Surface
+ private lateinit var previewProcessor: PreviewProcessor
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+ private lateinit var backgroundThread: HandlerThread
+ private lateinit var backgroundHandler: Handler
+ private lateinit var fakePreviewImageProcessorImpl: FakePreviewImageProcessorImpl
+
+ companion object {
+ const val CAMERA_ID = "0"
+ const val WIDTH = 640
+ const val HEIGHT = 480
+ const val MAX_IMAGES = 6
+ }
+
+ @Before
+ fun setUp() {
+ Assume.assumeTrue(CameraUtil.deviceHasCamera())
+
+ backgroundThread = HandlerThread(
+ CameraXThreads.TAG + "preview_processor_test"
+ )
+ backgroundThread.start()
+ backgroundHandler = Handler(backgroundThread.looper)
+ surfaceTexture = SurfaceTexture(0)
+ surfaceTexture.setDefaultBufferSize(WIDTH, HEIGHT)
+ surfaceTexture.detachFromGLContext()
+
+ previewSurface = Surface(surfaceTexture)
+ fakePreviewImageProcessorImpl = FakePreviewImageProcessorImpl()
+ previewProcessor = PreviewProcessor(
+ fakePreviewImageProcessorImpl, previewSurface, Size(WIDTH, HEIGHT)
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (::backgroundThread.isInitialized) {
+ backgroundThread.quitSafely()
+ }
+
+ if (::surfaceTexture.isInitialized) {
+ surfaceTexture.release()
+ }
+ if (::previewSurface.isInitialized) {
+ previewSurface.release()
+ }
+
+ if (::previewProcessor.isInitialized) {
+ previewProcessor.close()
+ }
+
+ if (::fakePreviewImageProcessorImpl.isInitialized) {
+ fakePreviewImageProcessorImpl.close()
+ }
+ }
+
+ @Test
+ fun canOutputToPreview(): Unit = runBlocking {
+ val cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+ val imageReaderYuv = ImageReader.newInstance(
+ WIDTH, HEIGHT, ImageFormat.YUV_420_888, MAX_IMAGES
+ )
+ val session = Camera2Util.openCaptureSession(
+ cameraDevice,
+ arrayListOf(imageReaderYuv.surface),
+ backgroundHandler
+ )
+
+ previewProcessor.start()
+
+ imageReaderYuv.setOnImageAvailableListener({
+ val image = it.acquireNextImage()
+ previewProcessor.notifyImage(createImageReference(image))
+ }, backgroundHandler)
+ Camera2Util.startRepeating(cameraDevice, session, arrayListOf(imageReaderYuv.surface)) {
+ previewProcessor.notifyCaptureResult(it)
+ }
+ val previewUpdateDeferred = CompletableDeferred<Boolean>()
+ surfaceTexture.setOnFrameAvailableListener {
+ previewUpdateDeferred.complete(true)
+ }
+
+ withTimeout(3000) {
+ assertTrue(previewUpdateDeferred.await())
+ }
+ }
+
+ @Test
+ fun canCloseProcessor(): Unit = runBlocking {
+ val cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+ val imageReaderYuv = ImageReader.newInstance(
+ WIDTH, HEIGHT, ImageFormat.YUV_420_888, MAX_IMAGES
+ )
+ val session = Camera2Util.openCaptureSession(
+ cameraDevice,
+ arrayListOf(imageReaderYuv.surface),
+ backgroundHandler
+ )
+
+ previewProcessor.start()
+
+ imageReaderYuv.setOnImageAvailableListener({
+ val image = it.acquireNextImage()
+ previewProcessor.notifyImage(createImageReference(image))
+ }, backgroundHandler)
+
+ Camera2Util.startRepeating(cameraDevice, session, arrayListOf(imageReaderYuv.surface)) {
+ previewProcessor.notifyCaptureResult(it)
+ }
+ val previewUpdateDeferred = CompletableDeferred<Boolean>()
+ surfaceTexture.setOnFrameAvailableListener {
+ previewUpdateDeferred.complete(true)
+ }
+
+ withTimeout(3000) {
+ previewUpdateDeferred.await()
+ }
+ previewProcessor.close()
+ // close the preview surface to see if closing causes any issues.
+ surfaceTexture.release()
+ previewSurface.release()
+
+ // Delay a little while to see if the close() causes any issue.
+ delay(1000)
+ }
+
+ private fun createImageReference(image: Image): ImageReference {
+ return object : ImageReference {
+ private var refCount = 1
+ override fun increment(): Boolean {
+ if (refCount <= 0) return false
+ refCount++
+ return true
+ }
+
+ override fun decrement(): Boolean {
+ if (refCount <= 0) return false
+ refCount--
+ if (refCount <= 0) {
+ image.close()
+ }
+ return true
+ }
+
+ override fun get(): Image? {
+ return image
+ }
+ }
+ }
+
+ private class FakePreviewImageProcessorImpl : PreviewImageProcessorImpl {
+ private var imageWriter: ImageWriter? = null
+ override fun process(image: Image?, result: TotalCaptureResult?) {
+ val emptyImage = imageWriter!!.dequeueInputImage()
+ imageWriter!!.queueInputImage(emptyImage)
+ }
+
+ override fun process(
+ image: Image?,
+ result: TotalCaptureResult?,
+ resultCallback: ProcessResultImpl?,
+ executor: Executor?
+ ) {
+ val blankImage = imageWriter!!.dequeueInputImage()
+ imageWriter!!.queueInputImage(blankImage)
+ }
+
+ override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+ imageWriter = ImageWriter.newInstance(surface, 2)
+ }
+
+ override fun onResolutionUpdate(size: Size) {
+ }
+
+ override fun onImageFormatUpdate(imageFormat: Int) {
+ }
+
+ fun close() {
+ imageWriter?.close()
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
new file mode 100644
index 0000000..401ad8d
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.TotalCaptureResult
+import android.media.Image
+import android.media.ImageReader
+import android.media.ImageWriter
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraXThreads
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.impl.ImageReaderProxy
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.extensions.impl.CaptureProcessorImpl
+import androidx.camera.extensions.impl.ProcessResultImpl
+import androidx.camera.extensions.internal.sessionprocessor.StillCaptureProcessor.OnCaptureResultCallback
+import androidx.camera.testing.Camera2Util
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 29) // Extensions supported on API 29+
+@RunWith(AndroidJUnit4::class)
+class StillCaptureProcessorTest {
+ private lateinit var fakeCaptureProcessorImpl: FakeCaptureProcessorImpl
+ private lateinit var stillCaptureProcessor: StillCaptureProcessor
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+ )
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+ private lateinit var backgroundThread: HandlerThread
+ private lateinit var backgroundHandler: Handler
+ private lateinit var imageReaderJpeg: ImageReaderProxy
+ private var cameraDevice: CameraDevice? = null
+ private var cameraYuvImageReader: ImageReader? = null
+ companion object {
+ const val CAMERA_ID = "0"
+ const val WIDTH = 640
+ const val HEIGHT = 480
+ }
+
+ @Before
+ fun setUp() {
+ Assume.assumeTrue(CameraUtil.deviceHasCamera())
+
+ cameraDevice?.close()
+ cameraYuvImageReader?.close()
+ backgroundThread = HandlerThread(
+ CameraXThreads.TAG + "still_capture_processor_test"
+ )
+ backgroundThread.start()
+ backgroundHandler = Handler(backgroundThread.looper)
+ fakeCaptureProcessorImpl = FakeCaptureProcessorImpl()
+ imageReaderJpeg = ImageReaderProxys.createIsolatedReader(WIDTH, HEIGHT, ImageFormat.JPEG, 2)
+ stillCaptureProcessor = StillCaptureProcessor(
+ fakeCaptureProcessorImpl, imageReaderJpeg.surface!!, Size(WIDTH, HEIGHT)
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (::stillCaptureProcessor.isInitialized) {
+ stillCaptureProcessor.close()
+ }
+ if (::backgroundThread.isInitialized) {
+ backgroundThread.quitSafely()
+ }
+ if (::imageReaderJpeg.isInitialized) {
+ imageReaderJpeg.close()
+ }
+
+ if (::fakeCaptureProcessorImpl.isInitialized) {
+ fakeCaptureProcessorImpl.close()
+ }
+ }
+
+ @Test
+ fun canOutputJpeg_3CaptureStages(): Unit = runBlocking {
+ withTimeout(10000) {
+ openCameraAndCaptureImageAwait(listOf(1, 2, 3))
+ }.use {
+ assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+ }
+ }
+
+ @Test
+ fun canOutputJpeg_1CaptureStage(): Unit = runBlocking {
+ withTimeout(10000) {
+ openCameraAndCaptureImageAwait(listOf(1))
+ }.use {
+ assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+ }
+ }
+
+ @Test
+ fun onErrorInvoked_oemProcessingFailed(): Unit = runBlocking {
+ fakeCaptureProcessorImpl.enableThrowExceptionDuringProcess()
+ assertThrows<Exception> {
+ withTimeout(3000) {
+ openCameraAndCaptureImageAwait(listOf(1)).close()
+ }
+ }
+ }
+
+ @Test
+ fun onErrorInvoked_jpegConversionFailed(): Unit = runBlocking {
+ val fakeYuvToJpegConverter = object : YuvToJpegConverter(100, imageReaderJpeg.surface!!) {
+ override fun writeYuvImage(imageProxy: ImageProxy) {
+ throw ConversionFailedException(
+ "Failed to convert JPEG to YUV", null)
+ }
+ }
+ stillCaptureProcessor = StillCaptureProcessor(
+ fakeCaptureProcessorImpl,
+ imageReaderJpeg.surface!!,
+ Size(WIDTH, HEIGHT),
+ fakeYuvToJpegConverter
+ )
+ assertThrows<Exception> {
+ withTimeout(3000) {
+ openCameraAndCaptureImageAwait(listOf(1)).close()
+ }
+ }
+ }
+
+ private suspend fun captureImage(
+ cameraDevice: CameraDevice,
+ cameraCaptureSession: CameraCaptureSession,
+ cameraYuvImageReader: ImageReader,
+ captureStageIdList: List<Int>
+ ): ImageProxy {
+
+ cameraYuvImageReader.setOnImageAvailableListener(
+ {
+ val image = it.acquireNextImage()
+ stillCaptureProcessor.notifyImage(createImageReference(image))
+ }, backgroundHandler
+ )
+ val deferredCaptureCompleted = CompletableDeferred<Unit>()
+ stillCaptureProcessor.startCapture(captureStageIdList, object : OnCaptureResultCallback {
+ override fun onCompleted() {
+ deferredCaptureCompleted.complete(Unit)
+ }
+
+ override fun onError(e: Exception) {
+ deferredCaptureCompleted.completeExceptionally(e)
+ }
+ })
+
+ val outputJpegDeferred = CompletableDeferred<ImageProxy>()
+ imageReaderJpeg.setOnImageAvailableListener({
+ val image = it.acquireNextImage()
+ outputJpegDeferred.complete(image!!)
+ }, CameraXExecutors.newHandlerExecutor(backgroundHandler))
+
+ captureStageIdList.forEach { captureStageId ->
+ val captureResult = Camera2Util.submitSingleRequest(
+ cameraDevice,
+ cameraCaptureSession,
+ listOf(cameraYuvImageReader.surface),
+ backgroundHandler
+ )
+ stillCaptureProcessor.notifyCaptureResult(captureResult, captureStageId)
+ }
+ deferredCaptureCompleted.await()
+ return outputJpegDeferred.await()
+ }
+
+ @Test
+ fun canStartCaptureMultipleTimes(): Unit = runBlocking {
+ val captureStageIdList = listOf(0, 1, 2)
+ cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+ cameraYuvImageReader = ImageReader.newInstance(
+ WIDTH, HEIGHT, ImageFormat.YUV_420_888,
+ captureStageIdList.size /* maxImages */
+ )
+ val captureSession = Camera2Util.openCaptureSession(
+ cameraDevice!!, listOf(cameraYuvImageReader!!.surface), backgroundHandler
+ )
+
+ withTimeout(30000) {
+ repeat(3) {
+ captureImage(
+ cameraDevice!!, captureSession, cameraYuvImageReader!!, listOf(0, 1, 2)
+ ).use {
+ assertThat(it).isNotNull()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun canSetRotation(): Unit = runBlocking {
+ val rotationDegrees = 270
+ withTimeout(10000) {
+ openCameraAndCaptureImageAwait(listOf(1), rotationDegrees = rotationDegrees)
+ }.use {
+ val exif = Exif.createFromImageProxy(it)
+ assertThat(exif.rotation).isEqualTo(rotationDegrees)
+ }
+ }
+
+ private suspend fun openCameraAndCaptureImageAwait(
+ captureStageIdList: List<Int>,
+ rotationDegrees: Int = 0,
+ onBeforeInputYuvReady: suspend () -> Unit = {},
+ onJpegProcessDone: suspend () -> Unit = {},
+ ): ImageProxy {
+ val (deferredCapture, deferredJpeg) = openCameraAndCaptureImage(
+ captureStageIdList,
+ rotationDegrees,
+ onBeforeInputYuvReady,
+ onJpegProcessDone
+ )
+ deferredCapture.await()
+ return deferredJpeg.await()
+ }
+
+ private suspend fun openCameraAndCaptureImage(
+ captureStageIdList: List<Int>,
+ rotationDegrees: Int = 0,
+ onBeforeInputYuvReady: suspend () -> Unit = {},
+ onJpegProcessDone: suspend () -> Unit = {},
+ ): Pair<Deferred<Unit>, Deferred<ImageProxy>> {
+ stillCaptureProcessor.setRotationDegrees(rotationDegrees)
+ cameraDevice = Camera2Util.openCameraDevice(cameraManager, CAMERA_ID, backgroundHandler)
+ cameraYuvImageReader = ImageReader.newInstance(
+ WIDTH, HEIGHT, ImageFormat.YUV_420_888,
+ captureStageIdList.size /* maxImages */
+ )
+ val captureSession = Camera2Util.openCaptureSession(
+ cameraDevice!!, listOf(cameraYuvImageReader!!.surface), backgroundHandler
+ )
+
+ val deferredCapture = CompletableDeferred<Unit>()
+ stillCaptureProcessor.startCapture(captureStageIdList, object : OnCaptureResultCallback {
+ override fun onCompleted() {
+ deferredCapture.complete(Unit)
+ }
+
+ override fun onError(e: java.lang.Exception) {
+ deferredCapture.completeExceptionally(e)
+ }
+ })
+
+ val deferredOutputJpeg = CompletableDeferred<ImageProxy>()
+ imageReaderJpeg.setOnImageAvailableListener({
+ val image = it.acquireNextImage()
+ deferredOutputJpeg.complete(image!!)
+ }, CameraXExecutors.newHandlerExecutor(backgroundHandler))
+
+ cameraYuvImageReader!!.setOnImageAvailableListener(
+ {
+ val image = it.acquireNextImage()
+ stillCaptureProcessor.notifyImage(createImageReference(image))
+ }, backgroundHandler
+ )
+
+ onBeforeInputYuvReady.invoke()
+
+ for (id in captureStageIdList) {
+ val captureResult = Camera2Util.submitSingleRequest(
+ cameraDevice!!,
+ captureSession,
+ listOf(cameraYuvImageReader!!.surface),
+ backgroundHandler
+ )
+ stillCaptureProcessor.notifyCaptureResult(captureResult, id)
+ }
+
+ onJpegProcessDone.invoke()
+
+ return Pair(deferredCapture, deferredOutputJpeg)
+ }
+
+ @Test
+ fun canCloseBeforeProcessing(): Unit = runBlocking {
+ withTimeout(3000) {
+ openCameraAndCaptureImage(
+ listOf(0, 1),
+ onBeforeInputYuvReady = {
+ // Close the StillCaptureProcessor before it starts the processing.
+ stillCaptureProcessor.close()
+ // Close output jpeg image reader to see if processing failed.
+ imageReaderJpeg.close()
+ },
+ onJpegProcessDone = {
+ // Delay a little while to see if close causes any issue
+ delay(1000)
+ }
+ )
+ }
+ }
+
+ @Test
+ fun canCloseBeforeJpegConversion(): Unit = runBlocking {
+ withTimeout(3000) {
+ openCameraAndCaptureImage(
+ listOf(0, 1),
+ onJpegProcessDone = {
+ // Close the StillCaptureProcessor before it starts the JPEG encoding.
+ stillCaptureProcessor.close()
+ // Close output jpeg image reader to see if processing failed.
+ imageReaderJpeg.close()
+ // Delay a little while to see if close causes any issue
+ delay(1000)
+ }
+ )
+ }
+ }
+
+ private fun createImageReference(image: Image): ImageReference {
+ return object : ImageReference {
+ private var refCount = 1
+ override fun increment(): Boolean {
+ if (refCount <= 0) return false
+ refCount++
+ return true
+ }
+
+ override fun decrement(): Boolean {
+ if (refCount <= 0) return false
+ refCount--
+ if (refCount <= 0) {
+ image.close()
+ }
+ return true
+ }
+
+ override fun get(): Image? {
+ return image
+ }
+ }
+ }
+
+ // A fake CaptureProcessorImpl that simply output a blank Image.
+ class FakeCaptureProcessorImpl : CaptureProcessorImpl {
+ private var imageWriter: ImageWriter? = null
+
+ private var throwExceptionDuringProcess = false
+
+ fun enableThrowExceptionDuringProcess() {
+ throwExceptionDuringProcess = true
+ }
+ override fun process(
+ results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>?
+ ) {
+ if (throwExceptionDuringProcess) {
+ throw RuntimeException("Process failed")
+ }
+ val image = imageWriter!!.dequeueInputImage()
+ imageWriter!!.queueInputImage(image)
+ }
+
+ override fun process(
+ results: MutableMap<Int, android.util.Pair<Image, TotalCaptureResult>>?,
+ resultCallback: ProcessResultImpl?,
+ executor: Executor?
+ ) {
+ process(results)
+ }
+
+ override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+ imageWriter = ImageWriter.newInstance(surface, 2)
+ }
+
+ override fun onResolutionUpdate(size: Size) {
+ }
+
+ override fun onImageFormatUpdate(imageFormat: Int) {
+ }
+
+ fun close() {
+ imageWriter?.close()
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt
new file mode 100644
index 0000000..b459806
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverterTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor
+
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.ImageReaderProxys
+import androidx.camera.core.ImmutableImageInfo
+import androidx.camera.core.impl.ImageReaderProxy
+import androidx.camera.core.impl.TagBundle
+import androidx.camera.core.impl.utils.Exif
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.TestImageUtil
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class YuvToJpegConverterTest {
+ companion object {
+ const val WIDTH = 640
+ const val HEIGHT = 480
+ const val MAX_IMAGES = 2
+ }
+
+ private lateinit var jpegImageReaderProxy: ImageReaderProxy
+ private lateinit var yuvToJpegConverter: YuvToJpegConverter
+
+ @Before
+ fun setUp() {
+ jpegImageReaderProxy = ImageReaderProxys.createIsolatedReader(
+ WIDTH, HEIGHT, ImageFormat.JPEG, MAX_IMAGES
+ )
+ yuvToJpegConverter = YuvToJpegConverter(
+ 100, jpegImageReaderProxy.surface!!
+ )
+ }
+
+ @After
+ fun tearDown() {
+ jpegImageReaderProxy.close()
+ }
+
+ private fun generateYuvImage(): ImageProxy {
+ return TestImageUtil.createYuvFakeImageProxy(
+ ImmutableImageInfo.create(
+ TagBundle.emptyBundle(), 0, 0, Matrix()
+ ), WIDTH, HEIGHT
+ )
+ }
+
+ @Test
+ fun canOutputJpeg() = runBlocking {
+ val deferredImage = CompletableDeferred<ImageProxy>()
+ jpegImageReaderProxy.setOnImageAvailableListener({ imageReader ->
+ imageReader.acquireNextImage()?.let { deferredImage.complete(it) }
+ }, CameraXExecutors.ioExecutor())
+
+ val imageYuv = generateYuvImage()
+ yuvToJpegConverter.writeYuvImage(imageYuv)
+
+ withTimeout(1000) {
+ deferredImage.await().use {
+ assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+ assertExifWidthAndHeight(it, WIDTH, HEIGHT)
+ }
+ }
+ }
+
+ private fun assertExifWidthAndHeight(imageProxy: ImageProxy, width: Int, height: Int) {
+ val exif = Exif.createFromImageProxy(imageProxy)
+ assertThat(exif.width).isEqualTo(width)
+ assertThat(exif.height).isEqualTo(height)
+ }
+
+ @Test
+ fun canSetRotation() = runBlocking {
+ val rotationDegrees = 270
+ val deferredImage = CompletableDeferred<ImageProxy>()
+ jpegImageReaderProxy.setOnImageAvailableListener({ imageReader ->
+ imageReader.acquireNextImage()?.let { deferredImage.complete(it) }
+ }, CameraXExecutors.ioExecutor())
+
+ val imageYuv = generateYuvImage()
+ yuvToJpegConverter.setRotationDegrees(rotationDegrees)
+ yuvToJpegConverter.writeYuvImage(imageYuv)
+
+ withTimeout(1000) {
+ deferredImage.await().use {
+ assertThat(it.format).isEqualTo(ImageFormat.JPEG)
+ val exif = Exif.createFromImageProxy(it)
+ assertThat(exif.rotation).isEqualTo(rotationDegrees)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
index f620986..512ebce 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
@@ -204,7 +204,7 @@
vendorExtender.init(cameraInfo);
ExtensionsUseCaseConfigFactory factory = new
- ExtensionsUseCaseConfigFactory(mode, vendorExtender, context);
+ ExtensionsUseCaseConfigFactory(mode, vendorExtender);
ExtensionsConfig.Builder builder = new ExtensionsConfig.Builder()
.setExtensionMode(mode)
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index 37b5610..f419244 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -20,6 +20,7 @@
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -47,8 +48,12 @@
import androidx.camera.extensions.impl.NightPreviewExtenderImpl;
import androidx.camera.extensions.impl.PreviewExtenderImpl;
import androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator;
+import androidx.camera.extensions.internal.sessionprocessor.BasicExtenderSessionProcessor;
import androidx.core.util.Preconditions;
+import org.jetbrains.annotations.TestOnly;
+
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -61,13 +66,11 @@
private static final String TAG = "BasicVendorExtender";
private final ExtensionDisabledValidator mExtensionDisabledValidator =
new ExtensionDisabledValidator();
- private final @ExtensionMode.Mode int mMode;
private PreviewExtenderImpl mPreviewExtenderImpl = null;
private ImageCaptureExtenderImpl mImageCaptureExtenderImpl = null;
private CameraInfo mCameraInfo;
public BasicVendorExtender(@ExtensionMode.Mode int mode) {
- mMode = mode;
try {
switch (mode) {
case ExtensionMode.BOKEH:
@@ -99,24 +102,11 @@
}
}
- /**
- * Return the {@link PreviewExtenderImpl} instance which could be null if the implementation
- * doesn't exist. This method will be removed once the existing basic extender implementation
- * is migrated to the unified vendor extender.
- */
- @Nullable
- public PreviewExtenderImpl getPreviewExtenderImpl() {
- return mPreviewExtenderImpl;
- }
-
- /**
- * Return the {@link ImageCaptureExtenderImpl} instance which could be null if the
- * implementation doesn't exist.. This method will be removed once the existing basic
- * extender implementation is migrated to the unified vendor extender.
- */
- @Nullable
- public ImageCaptureExtenderImpl getImageCaptureExtenderImpl() {
- return mImageCaptureExtenderImpl;
+ @TestOnly
+ BasicVendorExtender(ImageCaptureExtenderImpl imageCaptureExtenderImpl,
+ PreviewExtenderImpl previewExtenderImpl) {
+ mPreviewExtenderImpl = previewExtenderImpl;
+ mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
}
@Override
@@ -152,7 +142,6 @@
mPreviewExtenderImpl.init(cameraId, cameraCharacteristics);
mImageCaptureExtenderImpl.init(cameraId, cameraCharacteristics);
- Logger.d(TAG, "Extension init Mode = " + mMode);
Logger.d(TAG, "PreviewExtender processorType= " + mPreviewExtenderImpl.getProcessorType());
Logger.d(TAG, "ImageCaptureExtender processor= "
+ mImageCaptureExtenderImpl.getCaptureProcessor());
@@ -180,11 +169,7 @@
return map.getOutputSizes(imageFormat);
}
- private int getPreviewOutputImageFormat() {
- return ImageFormat.PRIVATE;
- }
-
- private int getCaptureOutputImageFormat() {
+ private int getCaptureInputImageFormat() {
if (mImageCaptureExtenderImpl != null
&& mImageCaptureExtenderImpl.getCaptureProcessor() != null) {
return ImageFormat.YUV_420_888;
@@ -193,6 +178,16 @@
}
}
+ private int getPreviewInputImageFormat() {
+ if (mPreviewExtenderImpl != null
+ && mPreviewExtenderImpl.getProcessorType()
+ == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+ return ImageFormat.YUV_420_888;
+ } else {
+ return ImageFormat.PRIVATE;
+ }
+ }
+
@NonNull
@Override
public List<Pair<Integer, Size[]>> getSupportedPreviewOutputResolutions() {
@@ -204,17 +199,25 @@
List<Pair<Integer, Size[]>> result =
mPreviewExtenderImpl.getSupportedResolutions();
if (result != null) {
- return result;
+ // Ensure the PRIVATE format is in the list.
+ // PreviewExtenderImpl.getSupportedResolutions() returns the supported size
+ // for input surface. We need to ensure output surface format is supported.
+ return replaceImageFormatIfMissing(result,
+ ImageFormat.YUV_420_888 /* formatToBeReplaced */,
+ ImageFormat.PRIVATE /* newFormat */);
}
} catch (NoSuchMethodError e) {
}
}
// Returns output sizes from stream configuration map if OEM returns null or OEM does not
- // implement the function. It is required to return all supported sizes so it must fetch
- // all sizes from the stream configuration map here.
- int imageformat = getPreviewOutputImageFormat();
- return Arrays.asList(new Pair<>(imageformat, getOutputSizes(imageformat)));
+ // implement the function. BasicVendorExtender's SessionProcessor will always output
+ // to PRIVATE surface, but the input image which connect to the camera could be
+ // either YUV or PRIVATE. Since the input image from input surface is guaranteed to be
+ // able to output to the output surface, therefore we fetch the sizes from the
+ // input image format for the output format.
+ int inputImageFormat = getPreviewInputImageFormat();
+ return Arrays.asList(new Pair<>(ImageFormat.PRIVATE, getOutputSizes(inputImageFormat)));
}
@@ -228,17 +231,51 @@
List<Pair<Integer, Size[]>> result =
mImageCaptureExtenderImpl.getSupportedResolutions();
if (result != null) {
- return result;
+ // Ensure the JPEG format is in the list.
+ // ImageCaptureExtenderImpl.getSupportedResolutions() returns the supported
+ // size for input surface. We need to ensure output surface format is supported.
+ return replaceImageFormatIfMissing(result,
+ ImageFormat.YUV_420_888 /* formatToBeReplaced */,
+ ImageFormat.JPEG /* newFormat */);
}
} catch (NoSuchMethodError e) {
}
}
// Returns output sizes from stream configuration map if OEM returns null or OEM does not
- // implement the function. It is required to return all supported sizes so it must fetch
- // all sizes from the stream configuration map here.
- int imageFormat = getCaptureOutputImageFormat();
- return Arrays.asList(new Pair<>(imageFormat, getOutputSizes(imageFormat)));
+ // implement the function. BasicVendorExtender's SessionProcessor will always output
+ // JPEG Images, but the input image which connect to the camera could be either YUV or
+ // JPEG. Since the input image from input surface is guaranteed to be able to output to
+ // the output surface, therefore we fetch the sizes from the input image format for the
+ // output format.
+ int inputImageFormat = getCaptureInputImageFormat();
+ return Arrays.asList(new Pair<>(ImageFormat.JPEG, getOutputSizes(inputImageFormat)));
+ }
+
+ private List<Pair<Integer, Size[]>> replaceImageFormatIfMissing(
+ List<Pair<Integer, Size[]>> input, int formatToBeReplaced, int newFormat) {
+ for (Pair<Integer, Size[]> pair : input) {
+ if (pair.first == newFormat) {
+ return input;
+ }
+ }
+
+ List<Pair<Integer, Size[]>> output = new ArrayList<>();
+ boolean formatFound = false;
+ for (Pair<Integer, Size[]> pair : input) {
+ if (pair.first == formatToBeReplaced) {
+ formatFound = true;
+ output.add(new Pair<>(newFormat, pair.second));
+ } else {
+ output.add(pair);
+ }
+ }
+
+ if (!formatFound) {
+ throw new IllegalArgumentException(
+ "Supported resolution should contain " + newFormat + " format.");
+ }
+ return output;
}
@NonNull
@@ -252,9 +289,11 @@
@Override
public SessionProcessor createSessionProcessor(@NonNull Context context) {
Preconditions.checkNotNull(mCameraInfo, "VendorExtender#init() must be called first");
- /* Return null to keep using existing flow for basic extender to ensure compatibility for
- * now. We will switch to SessionProcessor implementation once compatibility is ensured.
- */
- return null;
+ if (Build.VERSION.SDK_INT >= 26) {
+ return new BasicExtenderSessionProcessor(
+ mPreviewExtenderImpl, mImageCaptureExtenderImpl, context);
+ } else {
+ throw new IllegalArgumentException("SessionProcessor is not supported");
+ }
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
index 76bff3f..35f2a92 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
@@ -18,8 +18,6 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
-import android.content.Context;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -41,10 +39,9 @@
public ExtensionsUseCaseConfigFactory(
@ExtensionMode.Mode int mode,
- @NonNull VendorExtender vendorExtender,
- @NonNull Context context) {
- mImageCaptureConfigProvider = new ImageCaptureConfigProvider(mode, vendorExtender, context);
- mPreviewConfigProvider = new PreviewConfigProvider(mode, vendorExtender, context);
+ @NonNull VendorExtender vendorExtender) {
+ mImageCaptureConfigProvider = new ImageCaptureConfigProvider(mode, vendorExtender);
+ mPreviewConfigProvider = new PreviewConfigProvider(mode, vendorExtender);
}
/**
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index fa38f9c..f3c9824 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -16,38 +16,17 @@
package androidx.camera.extensions.internal;
-import android.content.Context;
-import android.hardware.camera2.CameraCharacteristics;
-import android.os.Build;
import android.util.Pair;
import android.util.Size;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
-import androidx.camera.core.CameraInfo;
import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Logger;
-import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.CaptureBundle;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.CaptureStage;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.extensions.ExtensionMode;
-import androidx.camera.extensions.impl.CaptureProcessorImpl;
-import androidx.camera.extensions.impl.CaptureStageImpl;
-import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
-import androidx.core.util.Preconditions;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -55,31 +34,26 @@
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ImageCaptureConfigProvider implements ConfigProvider<ImageCaptureConfig> {
- private static final String TAG = "ImageCaptureConfigProvider";
static final Config.Option<Integer> OPTION_IMAGE_CAPTURE_CONFIG_PROVIDER_MODE =
Config.Option.create("camerax.extensions.imageCaptureConfigProvider.mode",
Integer.class);
private final VendorExtender mVendorExtender;
- private final Context mContext;
@ExtensionMode.Mode
private final int mEffectMode;
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
public ImageCaptureConfigProvider(
@ExtensionMode.Mode int mode,
- @NonNull VendorExtender vendorExtender,
- @NonNull Context context) {
+ @NonNull VendorExtender vendorExtender) {
mEffectMode = mode;
mVendorExtender = vendorExtender;
- mContext = context;
}
@NonNull
@Override
public ImageCaptureConfig getConfig() {
ImageCapture.Builder builder = new ImageCapture.Builder();
- updateBuilderConfig(builder, mEffectMode, mVendorExtender, mContext);
+ updateBuilderConfig(builder, mEffectMode, mVendorExtender);
return builder.getUseCaseConfig();
}
@@ -87,41 +61,9 @@
/**
* Update extension related configs to the builder.
*/
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
void updateBuilderConfig(@NonNull ImageCapture.Builder builder,
- @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
- @NonNull Context context) {
- if (vendorExtender instanceof BasicVendorExtender) {
- ImageCaptureExtenderImpl imageCaptureExtenderImpl =
- ((BasicVendorExtender) vendorExtender).getImageCaptureExtenderImpl();
-
- if (imageCaptureExtenderImpl != null) {
- CaptureProcessorImpl captureProcessor =
- imageCaptureExtenderImpl.getCaptureProcessor();
- AdaptingCaptureProcessor adaptingCaptureProcessor = null;
- if (captureProcessor != null) {
- adaptingCaptureProcessor = new AdaptingCaptureProcessor(captureProcessor);
- builder.setCaptureProcessor(adaptingCaptureProcessor);
- }
-
- if (imageCaptureExtenderImpl.getMaxCaptureStage() > 0) {
- builder.setMaxCaptureStages(
- imageCaptureExtenderImpl.getMaxCaptureStage());
- }
-
- ImageCaptureEventAdapter imageCaptureEventAdapter =
- new ImageCaptureEventAdapter(imageCaptureExtenderImpl,
- context, adaptingCaptureProcessor);
- new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback(
- new CameraEventCallbacks(imageCaptureEventAdapter));
- builder.setUseCaseEventCallback(imageCaptureEventAdapter);
-
- builder.setCaptureBundle(imageCaptureEventAdapter);
- } else {
- Logger.e(TAG, "ImageCaptureExtenderImpl is null!");
- }
- }
-
+ @ExtensionMode.Mode int effectMode,
+ @NonNull VendorExtender vendorExtender) {
builder.getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_CONFIG_PROVIDER_MODE,
effectMode);
List<Pair<Integer, Size[]>> supportedResolutions =
@@ -129,122 +71,4 @@
builder.setSupportedResolutions(supportedResolutions);
builder.setHighResolutionDisabled(true);
}
-
-
- /**
- * An implementation to adapt the OEM provided implementation to core.
- */
- private static class ImageCaptureEventAdapter extends CameraEventCallback implements
- UseCase.EventCallback,
- CaptureBundle {
- @NonNull
- private final ImageCaptureExtenderImpl mImpl;
- @NonNull
- private final Context mContext;
- @Nullable
- private VendorProcessor mVendorCaptureProcessor;
- @Nullable
- private volatile CameraInfo mCameraInfo;
- ImageCaptureEventAdapter(@NonNull ImageCaptureExtenderImpl impl,
- @NonNull Context context,
- @Nullable VendorProcessor vendorCaptureProcessor) {
- mImpl = impl;
- mContext = context;
- mVendorCaptureProcessor = vendorCaptureProcessor;
- }
-
- // Invoked from main thread
- @Override
- public void onAttach(@NonNull CameraInfo cameraInfo) {
- mCameraInfo = cameraInfo;
- }
-
- // Invoked from main thread
- @Override
- public void onDetach() {
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
- public CaptureConfig onInitSession() {
- Preconditions.checkNotNull(mCameraInfo,
- "ImageCaptureConfigProvider was not attached.");
- String cameraId = Camera2CameraInfo.from(mCameraInfo).getCameraId();
- CameraCharacteristics cameraCharacteristics =
- Camera2CameraInfo.extractCameraCharacteristics(mCameraInfo);
- Logger.d(TAG, "ImageCapture onInit");
- mImpl.onInit(cameraId, cameraCharacteristics, mContext);
-
- if (mVendorCaptureProcessor != null) {
- mVendorCaptureProcessor.onInit();
- }
-
- CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
- if (captureStageImpl != null) {
- if (Build.VERSION.SDK_INT >= 28) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- } else {
- Logger.w(TAG, "The CaptureRequest parameters returned from "
- + "onPresetSession() will be passed to the camera device as part "
- + "of the capture session via "
- + "SessionConfiguration#setSessionParameters(CaptureRequest) "
- + "which only supported from API level 28!");
- }
- }
- return null;
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- public CaptureConfig onEnableSession() {
- Logger.d(TAG, "ImageCapture onEnableSession");
- CaptureStageImpl captureStageImpl = mImpl.onEnableSession();
- if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- }
-
- return null;
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- public CaptureConfig onDisableSession() {
- Logger.d(TAG, "ImageCapture onDisableSession");
- CaptureStageImpl captureStageImpl = mImpl.onDisableSession();
- if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- }
-
- return null;
- }
-
- // Invoked from main thread
- @Override
- @Nullable
- public List<CaptureStage> getCaptureStages() {
- List<CaptureStageImpl> captureStages = mImpl.getCaptureStages();
- if (captureStages != null && !captureStages.isEmpty()) {
- ArrayList<CaptureStage> ret = new ArrayList<>();
- for (CaptureStageImpl s : captureStages) {
- ret.add(new AdaptingCaptureStage(s));
- }
- return ret;
- }
-
- return null;
- }
-
- // Invoked from camera thread
- @Override
- public void onDeInitSession() {
- if (mVendorCaptureProcessor != null) {
- mVendorCaptureProcessor.onDeInit();
- }
- mImpl.onDeInit();
- }
- }
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index 2019e0b..f714479 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -16,35 +16,16 @@
package androidx.camera.extensions.internal;
-import android.content.Context;
-import android.hardware.camera2.CameraCharacteristics;
-import android.os.Build;
import android.util.Pair;
import android.util.Size;
-import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
-import androidx.camera.core.CameraInfo;
-import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
-import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.PreviewConfig;
import androidx.camera.extensions.ExtensionMode;
-import androidx.camera.extensions.impl.CaptureStageImpl;
-import androidx.camera.extensions.impl.PreviewExtenderImpl;
-import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
-import androidx.core.util.Preconditions;
import java.util.List;
@@ -57,211 +38,33 @@
static final Config.Option<Integer> OPTION_PREVIEW_CONFIG_PROVIDER_MODE = Config.Option.create(
"camerax.extensions.previewConfigProvider.mode", Integer.class);
private final VendorExtender mVendorExtender;
- private final Context mContext;
@ExtensionMode.Mode
private final int mEffectMode;
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
public PreviewConfigProvider(
@ExtensionMode.Mode int mode,
- @NonNull VendorExtender vendorExtender,
- @NonNull Context context) {
+ @NonNull VendorExtender vendorExtender) {
mEffectMode = mode;
mVendorExtender = vendorExtender;
- mContext = context;
}
@NonNull
@Override
public PreviewConfig getConfig() {
Preview.Builder builder = new Preview.Builder();
- updateBuilderConfig(builder, mEffectMode, mVendorExtender, mContext);
-
+ updateBuilderConfig(builder, mEffectMode, mVendorExtender);
return builder.getUseCaseConfig();
}
/**
* Update extension related configs to the builder.
*/
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
void updateBuilderConfig(@NonNull Preview.Builder builder,
- @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
- @NonNull Context context) {
- if (vendorExtender instanceof BasicVendorExtender) {
- PreviewExtenderImpl previewExtenderImpl =
- ((BasicVendorExtender) vendorExtender).getPreviewExtenderImpl();
-
- if (previewExtenderImpl != null) {
- PreviewEventAdapter previewEventAdapter;
-
- switch (previewExtenderImpl.getProcessorType()) {
- case PROCESSOR_TYPE_REQUEST_UPDATE_ONLY:
- AdaptingRequestUpdateProcessor adaptingRequestUpdateProcessor =
- new AdaptingRequestUpdateProcessor(previewExtenderImpl);
- builder.setImageInfoProcessor(adaptingRequestUpdateProcessor);
- previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
- adaptingRequestUpdateProcessor);
- break;
- case PROCESSOR_TYPE_IMAGE_PROCESSOR:
- AdaptingPreviewProcessor adaptingPreviewProcessor = new
- AdaptingPreviewProcessor(
- (PreviewImageProcessorImpl) previewExtenderImpl.getProcessor());
- builder.setCaptureProcessor(adaptingPreviewProcessor);
- builder.setIsRgba8888SurfaceRequired(true);
- previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
- adaptingPreviewProcessor);
- break;
- default:
- previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
- null);
- }
- new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback(
- new CameraEventCallbacks(previewEventAdapter));
- builder.setUseCaseEventCallback(previewEventAdapter);
- } else {
- Logger.e(TAG, "PreviewExtenderImpl is null!");
- }
- } else { // Advanced extensions interface.
- // Set RGB8888 = true always since we have no way to tell if the OEM implementation does
- // the processing or not.
- builder.setIsRgba8888SurfaceRequired(true);
- }
-
+ @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender) {
builder.getMutableConfig().insertOption(OPTION_PREVIEW_CONFIG_PROVIDER_MODE, effectMode);
List<Pair<Integer, Size[]>> supportedResolutions =
vendorExtender.getSupportedPreviewOutputResolutions();
builder.setSupportedResolutions(supportedResolutions);
builder.setHighResolutionDisabled(true);
}
-
- /**
- * An implementation to adapt the OEM provided implementation to core.
- */
- private static class PreviewEventAdapter extends CameraEventCallback implements
- UseCase.EventCallback {
- @NonNull
- final PreviewExtenderImpl mImpl;
- @NonNull
- private final Context mContext;
- @Nullable
- private final VendorProcessor mPreviewProcessor;
- @Nullable
- CameraInfo mCameraInfo;
- @GuardedBy("mLock")
- final Object mLock = new Object();
-
- PreviewEventAdapter(@NonNull PreviewExtenderImpl impl,
- @NonNull Context context, @Nullable VendorProcessor previewProcessor) {
- mImpl = impl;
- mContext = context;
- mPreviewProcessor = previewProcessor;
- }
-
- // Invoked from main thread
- @Override
- public void onAttach(@NonNull CameraInfo cameraInfo) {
- synchronized (mLock) {
- mCameraInfo = cameraInfo;
- }
- }
-
- // Invoked from main thread
- @Override
- public void onDetach() {
- synchronized (mLock) {
- if (mPreviewProcessor != null) {
- mPreviewProcessor.close();
- }
- }
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
- public CaptureConfig onInitSession() {
- synchronized (mLock) {
- Preconditions.checkNotNull(mCameraInfo,
- "PreviewConfigProvider was not attached.");
- String cameraId = Camera2CameraInfo.from(mCameraInfo).getCameraId();
- CameraCharacteristics cameraCharacteristics =
- Camera2CameraInfo.extractCameraCharacteristics(mCameraInfo);
- Logger.d(TAG, "Preview onInit");
- mImpl.onInit(cameraId, cameraCharacteristics, mContext);
- if (mPreviewProcessor != null) {
- mPreviewProcessor.onInit();
- }
- CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
- if (captureStageImpl != null) {
- if (Build.VERSION.SDK_INT >= 28) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- } else {
- Logger.w(TAG, "The CaptureRequest parameters returned from "
- + "onPresetSession() will be passed to the camera device as part "
- + "of the capture session via "
- + "SessionConfiguration#setSessionParameters(CaptureRequest) "
- + "which only supported from API level 28!");
- }
- }
- }
-
- return null;
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- public CaptureConfig onEnableSession() {
- synchronized (mLock) {
- Logger.d(TAG, "Preview onEnableSession");
- CaptureStageImpl captureStageImpl = mImpl.onEnableSession();
- if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- }
- }
-
- return null;
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- public CaptureConfig onDisableSession() {
- synchronized (mLock) {
- Logger.d(TAG, "Preview onDisableSession");
- CaptureStageImpl captureStageImpl = mImpl.onDisableSession();
- if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- }
- }
-
- return null;
- }
-
- // Invoked from camera thread
- @Override
- @Nullable
- public CaptureConfig onRepeating() {
- synchronized (mLock) {
- CaptureStageImpl captureStageImpl = mImpl.getCaptureStage();
- if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
- }
- }
-
- return null;
- }
-
- // Invoked from camera thread
- @Override
- public void onDeInitSession() {
- synchronized (mLock) {
- Logger.d(TAG, "Preview onDeInit");
- if (mPreviewProcessor != null) {
- mPreviewProcessor.onDeInit();
- }
- mImpl.onDeInit();
- }
- }
- }
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java
index e1f7380..8c882ac 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VendorExtender.java
@@ -69,7 +69,7 @@
Range<Long> getEstimatedCaptureLatencyRange(@Nullable Size size);
/**
- * Gets the supported output resolutions for preview.
+ * Gets the supported output resolutions for preview. PRIVATE format must be supported.
*
* <p>Pair list composed with {@link ImageFormat} and {@link Size} array will be returned.
*
@@ -84,7 +84,7 @@
List<Pair<Integer, Size[]>> getSupportedPreviewOutputResolutions();
/**
- * Gets the supported output resolutions for image capture.
+ * Gets the supported output resolutions for image capture. JPEG format must be supported.
*
* <p>Pair list composed with {@link ImageFormat} and {@link Size} array will be returned.
*
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
new file mode 100644
index 0000000..74de26b
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import static androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR;
+import static androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
+import androidx.camera.camera2.interop.CaptureRequestOptions;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureFailure;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.OutputSurface;
+import androidx.camera.core.impl.RequestProcessor;
+import androidx.camera.core.impl.SessionProcessor;
+import androidx.camera.extensions.impl.CaptureProcessorImpl;
+import androidx.camera.extensions.impl.CaptureStageImpl;
+import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
+import androidx.camera.extensions.impl.PreviewExtenderImpl;
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
+import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link SessionProcessor} based on OEMs' basic extender implementation.
+ */
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
+@RequiresApi(26) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class BasicExtenderSessionProcessor extends SessionProcessorBase {
+ private static final String TAG = "BasicSessionProcessor";
+
+ private static final int PREVIEW_PROCESS_MAX_IMAGES = 2;
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final PreviewExtenderImpl mPreviewExtenderImpl;
+ @NonNull
+ private final ImageCaptureExtenderImpl mImageCaptureExtenderImpl;
+
+ final Object mLock = new Object();
+ volatile StillCaptureProcessor mStillCaptureProcessor = null;
+ volatile PreviewProcessor mPreviewProcessor = null;
+ volatile RequestUpdateProcessorImpl mRequestUpdateProcessor = null;
+ private volatile Camera2OutputConfig mPreviewOutputConfig;
+ private volatile Camera2OutputConfig mCaptureOutputConfig;
+ @Nullable
+ private volatile Camera2OutputConfig mAnalysisOutputConfig = null;
+ private volatile OutputSurface mPreviewOutputSurfaceConfig;
+ private volatile OutputSurface mCaptureOutputSurfaceConfig;
+ private volatile RequestProcessor mRequestProcessor;
+ volatile boolean mIsCapturing = false;
+ private final AtomicInteger mNextCaptureSequenceId = new AtomicInteger(0);
+ static AtomicInteger sLastOutputConfigId = new AtomicInteger(0);
+ @GuardedBy("mLock")
+ private final Map<CaptureRequest.Key<?>, Object> mParameters = new LinkedHashMap<>();
+
+ public BasicExtenderSessionProcessor(@NonNull PreviewExtenderImpl previewExtenderImpl,
+ @NonNull ImageCaptureExtenderImpl imageCaptureExtenderImpl,
+ @NonNull Context context) {
+ mPreviewExtenderImpl = previewExtenderImpl;
+ mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
+ mContext = context;
+ }
+
+ @NonNull
+ @Override
+ protected Camera2SessionConfig initSessionInternal(@NonNull String cameraId,
+ @NonNull Map<String, CameraCharacteristics> cameraCharacteristicsMap,
+ @NonNull OutputSurface previewSurfaceConfig,
+ @NonNull OutputSurface imageCaptureSurfaceConfig,
+ @Nullable OutputSurface imageAnalysisSurfaceConfig) {
+ Logger.d(TAG, "PreviewExtenderImpl.onInit");
+ mPreviewExtenderImpl.onInit(cameraId, cameraCharacteristicsMap.get(cameraId),
+ mContext);
+ Logger.d(TAG, "ImageCaptureExtenderImpl.onInit");
+ mImageCaptureExtenderImpl.onInit(cameraId, cameraCharacteristicsMap.get(cameraId),
+ mContext);
+
+ mPreviewOutputSurfaceConfig = previewSurfaceConfig;
+ mCaptureOutputSurfaceConfig = imageCaptureSurfaceConfig;
+
+ // Preview
+ PreviewExtenderImpl.ProcessorType processorType =
+ mPreviewExtenderImpl.getProcessorType();
+ Logger.d(TAG, "preview processorType=" + processorType);
+ if (processorType == PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+ mPreviewOutputConfig = ImageReaderOutputConfig.create(
+ sLastOutputConfigId.getAndIncrement(),
+ previewSurfaceConfig.getSize(),
+ ImageFormat.YUV_420_888,
+ PREVIEW_PROCESS_MAX_IMAGES);
+ PreviewImageProcessorImpl previewImageProcessor =
+ (PreviewImageProcessorImpl) mPreviewExtenderImpl.getProcessor();
+ mPreviewProcessor = new PreviewProcessor(
+ previewImageProcessor, mPreviewOutputSurfaceConfig.getSurface(),
+ mPreviewOutputSurfaceConfig.getSize());
+ } else if (processorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
+ mPreviewOutputConfig = SurfaceOutputConfig.create(
+ sLastOutputConfigId.getAndIncrement(),
+ previewSurfaceConfig.getSurface());
+ mRequestUpdateProcessor =
+ (RequestUpdateProcessorImpl) mPreviewExtenderImpl.getProcessor();
+ } else {
+ mPreviewOutputConfig = SurfaceOutputConfig.create(
+ sLastOutputConfigId.getAndIncrement(),
+ previewSurfaceConfig.getSurface());
+ }
+
+ // Image Capture
+ CaptureProcessorImpl captureProcessor = mImageCaptureExtenderImpl.getCaptureProcessor();
+ Logger.d(TAG, "CaptureProcessor=" + captureProcessor);
+
+ if (captureProcessor != null) {
+ mCaptureOutputConfig = ImageReaderOutputConfig.create(
+ sLastOutputConfigId.getAndIncrement(),
+ imageCaptureSurfaceConfig.getSize(),
+ ImageFormat.YUV_420_888,
+ mImageCaptureExtenderImpl.getMaxCaptureStage());
+ mStillCaptureProcessor = new StillCaptureProcessor(
+ captureProcessor, mCaptureOutputSurfaceConfig.getSurface(),
+ mCaptureOutputSurfaceConfig.getSize());
+ } else {
+ mCaptureOutputConfig = SurfaceOutputConfig.create(
+ sLastOutputConfigId.getAndIncrement(),
+ imageCaptureSurfaceConfig.getSurface());
+ }
+
+ // Image Analysis
+ if (imageAnalysisSurfaceConfig != null) {
+ mAnalysisOutputConfig = SurfaceOutputConfig.create(
+ sLastOutputConfigId.getAndIncrement(),
+ imageAnalysisSurfaceConfig.getSurface());
+ }
+
+ Camera2SessionConfigBuilder builder =
+ new Camera2SessionConfigBuilder()
+ .addOutputConfig(mPreviewOutputConfig)
+ .addOutputConfig(mCaptureOutputConfig)
+ .setSessionTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+
+ if (mAnalysisOutputConfig != null) {
+ builder.addOutputConfig(mAnalysisOutputConfig);
+ }
+
+ CaptureStageImpl captureStagePreview = mPreviewExtenderImpl.onPresetSession();
+ Logger.d(TAG, "preview onPresetSession:" + captureStagePreview);
+
+ CaptureStageImpl captureStageCapture = mImageCaptureExtenderImpl.onPresetSession();
+ Logger.d(TAG, "capture onPresetSession:" + captureStageCapture);
+
+ if (captureStagePreview != null && captureStagePreview.getParameters() != null) {
+ for (Pair<CaptureRequest.Key, Object> parameter :
+ captureStagePreview.getParameters()) {
+ builder.addSessionParameter(parameter.first, parameter.second);
+ }
+ }
+
+ if (captureStageCapture != null && captureStageCapture.getParameters() != null) {
+ for (Pair<CaptureRequest.Key, Object> parameter :
+ captureStageCapture.getParameters()) {
+ builder.addSessionParameter(parameter.first, parameter.second);
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ protected void deInitSessionInternal() {
+ Logger.d(TAG, "preview onDeInit");
+ mPreviewExtenderImpl.onDeInit();
+ Logger.d(TAG, "capture onDeInit");
+ mImageCaptureExtenderImpl.onDeInit();
+
+ if (mPreviewProcessor != null) {
+ mPreviewProcessor.close();
+ mPreviewProcessor = null;
+ }
+ if (mStillCaptureProcessor != null) {
+ mStillCaptureProcessor.close();
+ mStillCaptureProcessor = null;
+ }
+ }
+
+ @Override
+ public void setParameters(@NonNull Config config) {
+ synchronized (mLock) {
+ HashMap<CaptureRequest.Key<?>, Object> map = new HashMap<>();
+
+ CaptureRequestOptions options =
+ CaptureRequestOptions.Builder.from(config).build();
+
+ for (Config.Option<?> option : options.listOptions()) {
+ @SuppressWarnings("unchecked")
+ CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+ map.put(key, options.retrieveOption(option));
+ }
+ mParameters.clear();
+ mParameters.putAll(map);
+ applyRotationAndJpegQualityToProcessor();
+ }
+ }
+
+ @Override
+ public void onCaptureSessionStart(@NonNull RequestProcessor requestProcessor) {
+ mRequestProcessor = requestProcessor;
+
+ List<CaptureStageImpl> captureStages = new ArrayList<>();
+ CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onEnableSession();
+ Logger.d(TAG, "preview onEnableSession: " + captureStage1);
+ if (captureStage1 != null) {
+ captureStages.add(captureStage1);
+ }
+ CaptureStageImpl captureStage2 = mImageCaptureExtenderImpl.onEnableSession();
+ Logger.d(TAG, "capture onEnableSession:" + captureStage2);
+ if (captureStage2 != null) {
+ captureStages.add(captureStage2);
+ }
+
+ if (!captureStages.isEmpty()) {
+ submitRequestByCaptureStages(requestProcessor, captureStages);
+ }
+
+ if (mPreviewProcessor != null) {
+ setImageProcessor(mPreviewOutputConfig.getId(),
+ new ImageProcessor() {
+ @Override
+ public void onNextImageAvailable(int outputStreamId, long timestampNs,
+ @NonNull ImageReference imageReference,
+ @Nullable String physicalCameraId) {
+ if (mPreviewProcessor != null) {
+ mPreviewProcessor.notifyImage(imageReference);
+ }
+ }
+ });
+ mPreviewProcessor.start();
+ }
+ }
+
+ private void applyParameters(RequestBuilder builder) {
+ synchronized (mLock) {
+ for (CaptureRequest.Key<?> key : mParameters.keySet()) {
+ Object value = mParameters.get(key);
+ if (value != null) {
+ builder.setParameters(key, value);
+ }
+ }
+ }
+ }
+
+ private void applyRotationAndJpegQualityToProcessor() {
+ synchronized (mLock) {
+ if (mStillCaptureProcessor == null) {
+ return;
+ }
+ Integer orientationObj = (Integer) mParameters.get(CaptureRequest.JPEG_ORIENTATION);
+ if (orientationObj != null) {
+ mStillCaptureProcessor.setRotationDegrees(orientationObj);
+ }
+
+ Byte qualityObj = (Byte) mParameters.get(CaptureRequest.JPEG_QUALITY);
+ if (qualityObj != null) {
+ mStillCaptureProcessor.setJpegQuality((int) qualityObj);
+ }
+ }
+ }
+
+
+ private void submitRequestByCaptureStages(RequestProcessor requestProcessor,
+ List<CaptureStageImpl> captureStageList) {
+ List<RequestProcessor.Request> requestList = new ArrayList<>();
+ for (CaptureStageImpl captureStage : captureStageList) {
+ RequestBuilder builder = new RequestBuilder();
+ builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+ if (mAnalysisOutputConfig != null) {
+ builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+ }
+ for (Pair<CaptureRequest.Key, Object> keyObjectPair : captureStage.getParameters()) {
+ builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+ }
+ builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+ requestList.add(builder.build());
+ }
+ requestProcessor.submit(requestList, new RequestProcessor.Callback() {
+ });
+ }
+
+ @Override
+ public void onCaptureSessionEnd() {
+ List<CaptureStageImpl> captureStages = new ArrayList<>();
+ CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onDisableSession();
+ Logger.d(TAG, "preview onDisableSession: " + captureStage1);
+ if (captureStage1 != null) {
+ captureStages.add(captureStage1);
+ }
+ CaptureStageImpl captureStage2 = mImageCaptureExtenderImpl.onDisableSession();
+ Logger.d(TAG, "capture onDisableSession:" + captureStage2);
+ if (captureStage2 != null) {
+ captureStages.add(captureStage2);
+ }
+
+ if (!captureStages.isEmpty()) {
+ submitRequestByCaptureStages(mRequestProcessor, captureStages);
+ }
+ mRequestProcessor = null;
+ mIsCapturing = false;
+ }
+
+ @Override
+ public int startRepeating(@NonNull CaptureCallback captureCallback) {
+ int repeatingCaptureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+ if (mRequestProcessor == null) {
+ captureCallback.onCaptureFailed(repeatingCaptureSequenceId);
+ captureCallback.onCaptureSequenceAborted(repeatingCaptureSequenceId);
+ } else {
+ updateRepeating(repeatingCaptureSequenceId, captureCallback);
+ }
+
+ return repeatingCaptureSequenceId;
+ }
+
+ void updateRepeating(int repeatingCaptureSequenceId, @NonNull CaptureCallback captureCallback) {
+ if (mRequestProcessor == null) {
+ Logger.d(TAG, "mRequestProcessor is null, ignore repeating request");
+ return;
+ }
+ RequestBuilder builder = new RequestBuilder();
+ builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+ if (mAnalysisOutputConfig != null) {
+ builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+ }
+ builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+ applyParameters(builder);
+ applyPreviewStagesParameters(builder);
+
+ RequestProcessor.Callback callback = new RequestProcessor.Callback() {
+ @Override
+ public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ CaptureResult captureResult =
+ Camera2CameraCaptureResultConverter.getCaptureResult(
+ cameraCaptureResult);
+ Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
+ "Cannot get TotalCaptureResult from the cameraCaptureResult ");
+ TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
+
+ if (mPreviewProcessor != null) {
+ mPreviewProcessor.notifyCaptureResult(totalCaptureResult);
+ }
+
+ if (mRequestUpdateProcessor != null) {
+ CaptureStageImpl captureStage =
+ mRequestUpdateProcessor.process(totalCaptureResult);
+
+ if (captureStage != null) {
+ updateRepeating(repeatingCaptureSequenceId, captureCallback);
+ }
+ }
+
+ captureCallback.onCaptureSequenceCompleted(repeatingCaptureSequenceId);
+ }
+ };
+
+ Logger.d(TAG, "requestProcessor setRepeating");
+ mRequestProcessor.setRepeating(builder.build(), callback);
+ }
+
+ private void applyPreviewStagesParameters(RequestBuilder builder) {
+ CaptureStageImpl captureStage = mPreviewExtenderImpl.getCaptureStage();
+ if (captureStage != null) {
+ for (Pair<CaptureRequest.Key, Object> keyObjectPair :
+ captureStage.getParameters()) {
+ builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+ }
+ }
+ }
+
+ @Override
+ public void stopRepeating() {
+ mRequestProcessor.stopRepeating();
+ }
+
+ @Override
+ public int startCapture(@NonNull CaptureCallback captureCallback) {
+ int captureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+
+ if (mRequestProcessor == null || mIsCapturing) {
+ Logger.d(TAG, "startCapture failed");
+ captureCallback.onCaptureFailed(captureSequenceId);
+ captureCallback.onCaptureSequenceAborted(captureSequenceId);
+ return captureSequenceId;
+ }
+ mIsCapturing = true;
+
+ List<RequestProcessor.Request> requestList = new ArrayList<>();
+ List<CaptureStageImpl> captureStages = mImageCaptureExtenderImpl.getCaptureStages();
+ List<Integer> captureIdList = new ArrayList<>();
+
+ for (CaptureStageImpl captureStage : captureStages) {
+ RequestBuilder builder = new RequestBuilder();
+ builder.addTargetOutputConfigIds(mCaptureOutputConfig.getId());
+ builder.setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ builder.setCaptureStageId(captureStage.getId());
+
+ captureIdList.add(captureStage.getId());
+
+ applyParameters(builder);
+ applyPreviewStagesParameters(builder);
+
+ for (Pair<CaptureRequest.Key, Object> keyObjectPair :
+ captureStage.getParameters()) {
+ builder.setParameters(keyObjectPair.first, keyObjectPair.second);
+ }
+ requestList.add(builder.build());
+ }
+
+ Logger.d(TAG, "Wait for capture stage id: " + captureIdList);
+
+ RequestProcessor.Callback callback = new RequestProcessor.Callback() {
+ boolean mIsCaptureFailed = false;
+ boolean mIsCaptureStarted = false;
+
+ @Override
+ public void onCaptureStarted(@NonNull RequestProcessor.Request request,
+ long frameNumber, long timestamp) {
+ if (!mIsCaptureStarted) {
+ mIsCaptureStarted = true;
+ captureCallback.onCaptureStarted(captureSequenceId, timestamp);
+ }
+ }
+
+ @Override
+ public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ CaptureResult captureResult =
+ Camera2CameraCaptureResultConverter.getCaptureResult(
+ cameraCaptureResult);
+ Preconditions.checkArgument(captureResult instanceof TotalCaptureResult,
+ "Cannot get capture TotalCaptureResult from the cameraCaptureResult ");
+ TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
+
+ RequestBuilder.RequestProcessorRequest requestProcessorRequest =
+ (RequestBuilder.RequestProcessorRequest) request;
+
+ if (mStillCaptureProcessor != null) {
+ mStillCaptureProcessor.notifyCaptureResult(
+ totalCaptureResult,
+ requestProcessorRequest.getCaptureStageId());
+ } else {
+ captureCallback.onCaptureProcessStarted(captureSequenceId);
+ captureCallback.onCaptureSequenceCompleted(captureSequenceId);
+ mIsCapturing = false;
+ }
+ }
+
+ @Override
+ public void onCaptureFailed(@NonNull RequestProcessor.Request request,
+ @NonNull CameraCaptureFailure captureFailure) {
+ if (!mIsCaptureFailed) {
+ mIsCaptureFailed = true;
+ captureCallback.onCaptureFailed(captureSequenceId);
+ captureCallback.onCaptureSequenceAborted(captureSequenceId);
+ mIsCapturing = false;
+ }
+ }
+
+ @Override
+ public void onCaptureSequenceAborted(int sequenceId) {
+ captureCallback.onCaptureSequenceAborted(captureSequenceId);
+ mIsCapturing = false;
+ }
+ };
+
+ Logger.d(TAG, "startCapture");
+ if (mStillCaptureProcessor != null) {
+ mStillCaptureProcessor.startCapture(captureIdList,
+ new StillCaptureProcessor.OnCaptureResultCallback() {
+ @Override
+ public void onCompleted() {
+ captureCallback.onCaptureSequenceCompleted(captureSequenceId);
+ mIsCapturing = false;
+ }
+
+ @Override
+ public void onError(@NonNull Exception e) {
+ captureCallback.onCaptureFailed(captureSequenceId);
+ mIsCapturing = false;
+ }
+ });
+ }
+ setImageProcessor(mCaptureOutputConfig.getId(),
+ new ImageProcessor() {
+ boolean mIsFirstFrame = true;
+
+ @Override
+ public void onNextImageAvailable(int outputStreamId, long timestampNs,
+ @NonNull ImageReference imageReference,
+ @Nullable String physicalCameraId) {
+ Logger.d(TAG,
+ "onNextImageAvailable outputStreamId=" + outputStreamId);
+ if (mStillCaptureProcessor != null) {
+ mStillCaptureProcessor.notifyImage(imageReference);
+ }
+
+ if (mIsFirstFrame) {
+ captureCallback.onCaptureProcessStarted(captureSequenceId);
+ mIsFirstFrame = false;
+ }
+ }
+ });
+ mRequestProcessor.submit(requestList, callback);
+ return captureSequenceId;
+ }
+
+ @Override
+ public void abortCapture(int captureSequenceId) {
+ mRequestProcessor.abortCaptures();
+ }
+
+ @Override
+ public int startTrigger(@NonNull Config config, @NonNull CaptureCallback callback) {
+ Logger.d(TAG, "startTrigger");
+ int captureSequenceId = mNextCaptureSequenceId.getAndIncrement();
+ RequestBuilder builder = new RequestBuilder();
+ builder.addTargetOutputConfigIds(mPreviewOutputConfig.getId());
+ if (mAnalysisOutputConfig != null) {
+ builder.addTargetOutputConfigIds(mAnalysisOutputConfig.getId());
+ }
+ builder.setTemplateId(CameraDevice.TEMPLATE_PREVIEW);
+ applyParameters(builder);
+ applyPreviewStagesParameters(builder);
+
+ CaptureRequestOptions options =
+ CaptureRequestOptions.Builder.from(config).build();
+ for (Config.Option<?> option : options.listOptions()) {
+ @SuppressWarnings("unchecked")
+ CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+ builder.setParameters(key, options.retrieveOption(option));
+ }
+
+ mRequestProcessor.submit(builder.build(), new RequestProcessor.Callback() {
+ @Override
+ public void onCaptureCompleted(@NonNull RequestProcessor.Request request,
+ @NonNull CameraCaptureResult captureResult) {
+ callback.onCaptureSequenceCompleted(captureSequenceId);
+ }
+
+ @Override
+ public void onCaptureFailed(@NonNull RequestProcessor.Request request,
+ @NonNull CameraCaptureFailure captureFailure) {
+ callback.onCaptureFailed(captureSequenceId);
+ }
+ });
+
+ return captureSequenceId;
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
index 60e13b7..57baff7 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/Camera2SessionConfigBuilder.java
@@ -55,8 +55,8 @@
* Sets session parameters.
*/
@NonNull
- <T> Camera2SessionConfigBuilder addSessionParameter(
- @NonNull CaptureRequest.Key<T> key, @Nullable T value) {
+ Camera2SessionConfigBuilder addSessionParameter(
+ @NonNull CaptureRequest.Key key, @Nullable Object value) {
mSessionParameters.put(key, value);
return this;
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/CaptureResultImageMatcher.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/CaptureResultImageMatcher.java
new file mode 100644
index 0000000..96b2bd5
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/CaptureResultImageMatcher.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.media.Image;
+import android.util.LongSparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * To match {@link ImageReference} with {@link TotalCaptureResult} by timestamp.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class CaptureResultImageMatcher {
+ private final Object mLock = new Object();
+ private static final int INVALID_TIMESTAMP = -1;
+ /** TotalCaptureResults that haven't been matched with Image. */
+ @GuardedBy("mLock")
+ private final LongSparseArray<TotalCaptureResult> mPendingCaptureResults =
+ new LongSparseArray<>();
+
+ /** To store the capture stage ids for each TotalCaptureResult */
+ @GuardedBy("mLock")
+ Map<TotalCaptureResult, Integer> mCaptureStageIdMap = new HashMap<>();
+
+ /** Images that haven't been matched with timestamp. */
+ @GuardedBy("mLock")
+ private final LongSparseArray<ImageReference> mPendingImages = new LongSparseArray<>();
+
+ @GuardedBy("mLock")
+ ImageReferenceListener mImageReferenceListener;
+
+ CaptureResultImageMatcher() {
+ }
+
+ void clear() {
+ synchronized (mLock) {
+ mPendingCaptureResults.clear();
+ for (int i = 0; i < mPendingImages.size(); i++) {
+ long key = mPendingImages.keyAt(i);
+ mPendingImages.get(key).decrement();
+ }
+ mPendingImages.clear();
+ mCaptureStageIdMap.clear();
+ }
+ }
+
+ void setImageReferenceListener(
+ @NonNull ImageReferenceListener imageReferenceListener) {
+ synchronized (mLock) {
+ mImageReferenceListener = imageReferenceListener;
+ }
+ }
+
+ void clearImageReferenceListener() {
+ synchronized (mLock) {
+ mImageReferenceListener = null;
+ }
+ }
+
+ void imageIncoming(@NonNull ImageReference imageReference) {
+ synchronized (mLock) {
+ Image image = imageReference.get();
+ mPendingImages.put(image.getTimestamp(), imageReference);
+ }
+ matchImages();
+ }
+
+ void captureResultIncoming(@NonNull TotalCaptureResult captureResult) {
+ captureResultIncoming(captureResult, 0);
+ }
+
+ void captureResultIncoming(@NonNull TotalCaptureResult captureResult,
+ int captureStageId) {
+ synchronized (mLock) {
+ long timestamp = getTimeStampFromCaptureResult(captureResult);
+ if (timestamp == INVALID_TIMESTAMP) {
+ return;
+ }
+ // Add the incoming CameraCaptureResult to pending list and do the matching logic.
+ mPendingCaptureResults.put(timestamp, captureResult);
+ mCaptureStageIdMap.put(captureResult, captureStageId);
+ }
+ matchImages();
+ }
+
+ private long getTimeStampFromCaptureResult(TotalCaptureResult captureResult) {
+ Long timestamp = captureResult.get(CaptureResult.SENSOR_TIMESTAMP);
+ long timestampValue = INVALID_TIMESTAMP;
+ if (timestamp != null) {
+ timestampValue = timestamp;
+ }
+
+ return timestampValue;
+ }
+
+
+ private void notifyImage(ImageReference imageReference,
+ TotalCaptureResult totalCaptureResult) {
+ ImageReferenceListener listenerToInvoke = null;
+ Integer captureStageId = null;
+ synchronized (mLock) {
+ if (mImageReferenceListener != null) {
+ listenerToInvoke = mImageReferenceListener;
+ captureStageId = mCaptureStageIdMap.get(totalCaptureResult);
+ } else {
+ imageReference.decrement();
+ }
+ }
+
+ if (listenerToInvoke != null) {
+ listenerToInvoke.onImageReferenceIncoming(imageReference,
+ totalCaptureResult, captureStageId);
+ }
+ }
+
+ // Remove the stale {@link ImageReference} from the pending queue if there
+ // are any missing which can happen if the camera is momentarily shut off.
+ // The ImageReference timestamps are assumed to be monotonically increasing. This
+ // means any ImageReference which has a timestamp older (smaller in value) than the
+ // oldest timestamp in the other queue will never get matched, so they should be removed.
+ //
+ // This should only be called at the end of matchImages(). The assumption is that there are no
+ // matching timestamps.
+ private void removeStaleData() {
+ synchronized (mLock) {
+ // No stale data to remove
+ if (mPendingImages.size() == 0 || mPendingCaptureResults.size() == 0) {
+ return;
+ }
+
+ Long minImageRefTimestamp = mPendingImages.keyAt(0);
+ Long minCaptureResultTimestamp = mPendingCaptureResults.keyAt(0);
+
+ // If timestamps are equal then matchImages did not correctly match up the capture
+ // result and Image
+ Preconditions.checkArgument(!minCaptureResultTimestamp.equals(minImageRefTimestamp));
+
+ if (minCaptureResultTimestamp > minImageRefTimestamp) {
+ for (int i = mPendingImages.size() - 1; i >= 0; i--) {
+ if (mPendingImages.keyAt(i) < minCaptureResultTimestamp) {
+ ImageReference imageReference = mPendingImages.valueAt(i);
+ imageReference.decrement();
+ mPendingImages.removeAt(i);
+ }
+ }
+ } else {
+ for (int i = mPendingCaptureResults.size() - 1; i >= 0; i--) {
+ if (mPendingCaptureResults.keyAt(i) < minImageRefTimestamp) {
+ mPendingCaptureResults.removeAt(i);
+ }
+ }
+ }
+ }
+ }
+
+ private void matchImages() {
+ ImageReference imageToNotify = null;
+ TotalCaptureResult resultToNotify = null;
+ synchronized (mLock) {
+ // Iterate in reverse order so that capture result can be removed in place
+ for (int i = mPendingCaptureResults.size() - 1; i >= 0; i--) {
+ TotalCaptureResult captureResult = mPendingCaptureResults.valueAt(i);
+ long timestamp = getTimeStampFromCaptureResult(captureResult);
+
+ ImageReference imageReference = mPendingImages.get(timestamp);
+
+ if (imageReference != null) {
+ mPendingImages.remove(timestamp);
+ mPendingCaptureResults.removeAt(i);
+ imageToNotify = imageReference;
+ resultToNotify = captureResult;
+ }
+ }
+ removeStaleData();
+ }
+
+ if (imageToNotify != null && resultToNotify != null) {
+ notifyImage(imageToNotify, resultToNotify);
+ }
+ }
+
+ interface ImageReferenceListener {
+ void onImageReferenceIncoming(@NonNull ImageReference imageReference,
+ @NonNull TotalCaptureResult totalCaptureResult, int captureStageId);
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
index 454e4a0..1ae12a1 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/ImageReaderOutputConfig.java
@@ -24,6 +24,7 @@
import com.google.auto.value.AutoValue;
+import java.util.Collections;
import java.util.List;
/**
@@ -42,6 +43,12 @@
return new AutoValue_ImageReaderOutputConfig(id, surfaceGroupId, physicalCameraId,
sharedOutputConfigs, size, imageFormat, maxImages);
}
+
+ static ImageReaderOutputConfig create(
+ int id, @NonNull Size size, int imageFormat, int maxImages) {
+ return new AutoValue_ImageReaderOutputConfig(id, -1, null,
+ Collections.emptyList(), size, imageFormat, maxImages);
+ }
/**
* Returns the size of the surface.
*/
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
new file mode 100644
index 0000000..0f34acb
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.TotalCaptureResult;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
+
+/**
+ * A preview processor that is responsible for invoking OEM's PreviewImageProcessorImpl and
+ * output to the given PRIVATE surface.
+ *
+ * <p>To start the processing, invoke {@link #start()} and then feed {@link ImageReference} and
+ * {@link TotalCaptureResult} instances by {@link #notifyImage(ImageReference)} and
+ * {@link #notifyCaptureResult(TotalCaptureResult)} respectively. The output will be written to the
+ * given output surface. Invoke {@link #close()} to close the processor and reclaim the resources.
+ *
+ * <p>Please note that output preview surface must be closed AFTER this processor is closed.
+ */
+@RequiresApi(26)
+class PreviewProcessor {
+ private static final String TAG = "PreviewProcessor";
+ @NonNull
+ final PreviewImageProcessorImpl mPreviewImageProcessor;
+ @NonNull
+ final CaptureResultImageMatcher mCaptureResultImageMatcher = new CaptureResultImageMatcher();
+ final Object mLock = new Object();
+ @GuardedBy("mLock")
+ boolean mIsClosed = false;
+
+ PreviewProcessor(@NonNull PreviewImageProcessorImpl previewImageProcessor,
+ @NonNull Surface previewOutputSurface, @NonNull Size surfaceSize) {
+ mPreviewImageProcessor = previewImageProcessor;
+ mPreviewImageProcessor.onResolutionUpdate(surfaceSize);
+ mPreviewImageProcessor.onOutputSurface(previewOutputSurface, PixelFormat.RGBA_8888);
+ mPreviewImageProcessor.onImageFormatUpdate(ImageFormat.YUV_420_888);
+ }
+
+ void start() {
+ mCaptureResultImageMatcher.setImageReferenceListener(
+ (imageReference, totalCaptureResult, captureStageId) -> {
+ synchronized (mLock) {
+ if (mIsClosed) {
+ imageReference.decrement();
+ Logger.d(TAG, "Ignore image in closed state");
+ return;
+ }
+ mPreviewImageProcessor.process(imageReference.get(),
+ totalCaptureResult);
+ imageReference.decrement();
+ }
+ });
+ }
+
+ void notifyCaptureResult(@NonNull TotalCaptureResult captureResult) {
+ mCaptureResultImageMatcher.captureResultIncoming(captureResult);
+ }
+
+ void notifyImage(@NonNull ImageReference imageReference) {
+ mCaptureResultImageMatcher.imageIncoming(imageReference);
+ }
+
+ /**
+ * Close the processor. Please note that output preview surface must be closed AFTER this
+ * processor is closed.
+ */
+ void close() {
+ synchronized (mLock) {
+ mIsClosed = true;
+ mCaptureResultImageMatcher.clear();
+ mCaptureResultImageMatcher.clearImageReferenceListener();
+ }
+ }
+
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java
new file mode 100644
index 0000000..32d17b9
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/RequestBuilder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.RequestProcessor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A builder for building {@link androidx.camera.core.impl.RequestProcessor.Request}.
+ */
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class RequestBuilder {
+ private List<Integer> mTargetOutputConfigIds = new ArrayList<>();
+ private Map<CaptureRequest.Key<?>, Object> mParameters = new HashMap<>();
+ private int mTemplateId = CameraDevice.TEMPLATE_PREVIEW;
+ int mCaptureStageId;
+
+ RequestBuilder() {
+ }
+
+ @NonNull
+ RequestBuilder addTargetOutputConfigIds(int targetOutputConfigId) {
+ mTargetOutputConfigIds.add(targetOutputConfigId);
+ return this;
+ }
+
+ @NonNull
+ RequestBuilder setParameters(@NonNull CaptureRequest.Key<?> key,
+ @NonNull Object value) {
+ mParameters.put(key, value);
+ return this;
+ }
+
+ @NonNull
+ RequestBuilder setTemplateId(int templateId) {
+ mTemplateId = templateId;
+ return this;
+ }
+
+ @NonNull
+ public RequestBuilder setCaptureStageId(int captureStageId) {
+ mCaptureStageId = captureStageId;
+ return this;
+ }
+
+ @NonNull
+ RequestProcessor.Request build() {
+ return new RequestProcessorRequest(
+ mTargetOutputConfigIds, mParameters, mTemplateId, mCaptureStageId);
+ }
+
+ static class RequestProcessorRequest implements RequestProcessor.Request {
+ final List<Integer> mTargetOutputConfigIds;
+ final Config mParameterConfig;
+ final int mTemplateId;
+ final int mCaptureStageId;
+
+ RequestProcessorRequest(List<Integer> targetOutputConfigIds,
+ Map<CaptureRequest.Key<?>, Object> parameters,
+ int templateId,
+ int captureStageId) {
+ mTargetOutputConfigIds = targetOutputConfigIds;
+ mTemplateId = templateId;
+ mCaptureStageId = captureStageId;
+
+ Camera2ImplConfig.Builder camera2ConfigBuilder = new Camera2ImplConfig.Builder();
+ for (CaptureRequest.Key<?> key : parameters.keySet()) {
+ @SuppressWarnings("unchecked")
+ CaptureRequest.Key<Object> objKey = (CaptureRequest.Key<Object>) key;
+ camera2ConfigBuilder.setCaptureRequestOption(objKey,
+ parameters.get(objKey));
+ }
+ mParameterConfig = camera2ConfigBuilder.build();
+ }
+
+ @Override
+ @NonNull
+ public List<Integer> getTargetOutputConfigIds() {
+ return mTargetOutputConfigIds;
+ }
+
+ @Override
+ @NonNull
+ public Config getParameters() {
+ return mParameterConfig;
+ }
+
+ @Override
+ public int getTemplateId() {
+ return mTemplateId;
+ }
+
+ public int getCaptureStageId() {
+ return mCaptureStageId;
+ }
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
new file mode 100644
index 0000000..d3f47fa
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.TotalCaptureResult;
+import android.media.Image;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.Camera2CameraCaptureResult;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageReaderProxys;
+import androidx.camera.core.Logger;
+import androidx.camera.core.SettableImageProxy;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.ImageReaderProxy;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.CameraCaptureResultImageInfo;
+import androidx.camera.extensions.impl.CaptureProcessorImpl;
+
+import org.jetbrains.annotations.TestOnly;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * A processor that is responsible for invoking OEM's CaptureProcessorImpl and converting the
+ * processed YUV images into JPEG images. The JPEG images are written to the given output surface.
+ *
+ * <p> It only outputs JPEG format images, meaning that the given output surface must be of JPEG
+ * format. To use this processor, follow the steps below:
+ * <pre>
+ * 1. Invoke start() with the required capture stage id list. A listener is given to
+ * be invoked when the processing is done and the resulting image is ready to appear in the
+ * output surface.
+ * 2. When input images from camera and TotalCaptureResult arrives, notify the processor by
+ * {@link #notifyImage(ImageReference)} and {@link #notifyCaptureResult(TotalCaptureResult, int)}.
+ * 3. Invoke {@link #setJpegQuality(int)} to adjust the jpeg quality and invoke
+ * {@link #setRotationDegrees(int)} if the device rotation changes.
+ * 4. When camera session is finishing, invoke {@link #close()} to reclaim the resources.
+ * Please note that the output JPEG surface should be closed AFTER this processor is closed().
+ * </pre>
+ */
+@RequiresApi(26)
+class StillCaptureProcessor {
+ private static final String TAG = "StillCaptureProcessor";
+ private static final int MAX_IMAGES = 2;
+ @NonNull
+ final CaptureProcessorImpl mCaptureProcessorImpl;
+ @NonNull
+ final CaptureResultImageMatcher mCaptureResultImageMatcher = new CaptureResultImageMatcher();
+ @NonNull
+ final ImageReaderProxy mProcessedYuvImageReader;
+ @NonNull
+ YuvToJpegConverter mYuvToJpegConverter;
+
+ final Object mLock = new Object();
+ @GuardedBy("mLock")
+ @NonNull
+ HashMap<Integer, Pair<ImageReference, TotalCaptureResult>> mCaptureResults =
+ new HashMap<>();
+
+ @GuardedBy("mLock")
+ OnCaptureResultCallback mOnCaptureResultCallback = null;
+ // Stores the first capture result for injecting into the output JPEG ImageProxy.
+ @GuardedBy("mLock")
+ TotalCaptureResult mSourceCaptureResult = null;
+ @GuardedBy("mLock")
+ boolean mIsClosed = false;
+ StillCaptureProcessor(@NonNull CaptureProcessorImpl captureProcessorImpl,
+ @NonNull Surface captureOutputSurface,
+ @NonNull Size surfaceSize) {
+ mCaptureProcessorImpl = captureProcessorImpl;
+ /*
+ Processing flow:
+ --> Collecting YUV images (from camera) via notifyImage and TotalCaptureResults
+ via notifyCaptureResult
+ --> mCaptureProcessorImpl.process (OEM process)
+ --> mProcessedYuvImageReader
+ --> mYuvToJpegProcessor (written to captureOutputSurface)
+ */
+ mProcessedYuvImageReader = ImageReaderProxys.createIsolatedReader(
+ surfaceSize.getWidth(),
+ surfaceSize.getHeight(),
+ ImageFormat.YUV_420_888, MAX_IMAGES);
+
+ mYuvToJpegConverter = new YuvToJpegConverter(100, captureOutputSurface);
+ mProcessedYuvImageReader.setOnImageAvailableListener(
+ imageReader -> {
+ OnCaptureResultCallback onCaptureResultCallback = null;
+ Exception errorException = null;
+ synchronized (mLock) {
+ if (mIsClosed) {
+ Logger.d(TAG, "Ignore JPEG processing in closed state");
+ return;
+ }
+ ImageProxy imageProxy = imageReader.acquireNextImage();
+ if (mSourceCaptureResult != null) {
+ imageProxy = new SettableImageProxy(imageProxy, null,
+ new CameraCaptureResultImageInfo(
+ new Camera2CameraCaptureResult(mSourceCaptureResult)));
+ mSourceCaptureResult = null;
+ }
+ if (imageProxy != null) {
+ try {
+ mYuvToJpegConverter.writeYuvImage(imageProxy);
+ } catch (YuvToJpegConverter.ConversionFailedException e) {
+ errorException = e;
+ }
+
+ if (mOnCaptureResultCallback != null) {
+ onCaptureResultCallback = mOnCaptureResultCallback;
+ mOnCaptureResultCallback = null;
+ }
+ }
+ }
+
+ if (onCaptureResultCallback != null) {
+ if (errorException != null) {
+ onCaptureResultCallback.onError(errorException);
+ } else {
+ onCaptureResultCallback.onCompleted();
+ }
+ }
+ }, CameraXExecutors.ioExecutor());
+
+ mCaptureProcessorImpl.onOutputSurface(mProcessedYuvImageReader.getSurface(),
+ ImageFormat.YUV_420_888);
+ mCaptureProcessorImpl.onImageFormatUpdate(ImageFormat.YUV_420_888);
+ mCaptureProcessorImpl.onResolutionUpdate(surfaceSize);
+ }
+
+ @TestOnly
+ StillCaptureProcessor(@NonNull CaptureProcessorImpl captureProcessorImpl,
+ @NonNull Surface captureOutputSurface,
+ @NonNull Size surfaceSize,
+ @NonNull YuvToJpegConverter yuvToJpegConverter) {
+ this(captureProcessorImpl, captureOutputSurface, surfaceSize);
+ mYuvToJpegConverter = yuvToJpegConverter;
+ }
+
+ interface OnCaptureResultCallback {
+ void onCompleted();
+ void onError(@NonNull Exception e);
+ }
+
+ void clearCaptureResults() {
+ synchronized (mLock) {
+ for (Pair<ImageReference, TotalCaptureResult> value :
+ mCaptureResults.values()) {
+ value.first.decrement();
+ }
+ mCaptureResults.clear();
+ }
+ }
+ void startCapture(@NonNull List<Integer> captureIdList,
+ @NonNull OnCaptureResultCallback onCaptureResultCallback) {
+ Logger.d(TAG, "Start the processor");
+
+ synchronized (mLock) {
+ mOnCaptureResultCallback = onCaptureResultCallback;
+ clearCaptureResults();
+ }
+
+ mCaptureResultImageMatcher.clear();
+ mCaptureResultImageMatcher.setImageReferenceListener(
+ (imageReference, totalCaptureResult, captureStageId) -> {
+ Exception errorException = null;
+ synchronized (mLock) {
+ if (mIsClosed) {
+ imageReference.decrement();
+ Logger.d(TAG, "Ignore image in closed state");
+ return;
+ }
+ Logger.d(TAG, "onImageReferenceIncoming captureStageId=" + captureStageId);
+
+ mCaptureResults.put(captureStageId, new Pair<>(imageReference,
+ totalCaptureResult));
+
+ Logger.d(TAG, "mCaptureResult has capture stage Id: "
+ + mCaptureResults.keySet());
+ if (mCaptureResults.keySet().containsAll(captureIdList)) {
+ HashMap<Integer, Pair<Image, TotalCaptureResult>> convertedResult =
+ new HashMap<>();
+ for (Integer id : mCaptureResults.keySet()) {
+ Pair<ImageReference, TotalCaptureResult> pair =
+ mCaptureResults.get(id);
+ convertedResult.put(id,
+ new Pair<>(pair.first.get(), pair.second));
+ }
+ Logger.d(TAG, "CaptureProcessorImpl.process()");
+ try {
+ mCaptureProcessorImpl.process(convertedResult);
+ } catch (Exception e) {
+ mOnCaptureResultCallback = null;
+ errorException = e;
+ }
+ clearCaptureResults();
+ }
+ }
+ if (errorException != null) {
+ if (onCaptureResultCallback != null) {
+ onCaptureResultCallback.onError(errorException);
+ }
+ }
+ });
+ }
+
+ void notifyCaptureResult(@NonNull TotalCaptureResult captureResult,
+ int captureStageId) {
+ mCaptureResultImageMatcher.captureResultIncoming(captureResult,
+ captureStageId);
+
+ synchronized (mLock) {
+ if (mSourceCaptureResult == null) {
+ mSourceCaptureResult = captureResult;
+ }
+ }
+ }
+
+ void notifyImage(@NonNull ImageReference imageReference) {
+ mCaptureResultImageMatcher.imageIncoming(imageReference);
+ }
+
+ void setJpegQuality(@IntRange(from = 0, to = 100) int quality) {
+ mYuvToJpegConverter.setJpegQuality(quality);
+ }
+
+ void setRotationDegrees(
+ @ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
+ mYuvToJpegConverter.setRotationDegrees(rotationDegrees);
+ }
+
+ /**
+ * Close the processor. Please note that captureOutputSurface passed in must be closed AFTER
+ * invoking this function.
+ */
+ void close() {
+ Logger.d(TAG, "Close the processor");
+ synchronized (mLock) {
+ mIsClosed = true;
+ clearCaptureResults();
+ mProcessedYuvImageReader.clearOnImageAvailableListener();
+ mCaptureResultImageMatcher.clearImageReferenceListener();
+ mCaptureResultImageMatcher.clear();
+ mProcessedYuvImageReader.close();
+ }
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
index 31a7144..4c56295 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/SurfaceOutputConfig.java
@@ -24,6 +24,7 @@
import com.google.auto.value.AutoValue;
+import java.util.Collections;
import java.util.List;
/**
@@ -44,6 +45,10 @@
sharedOutputConfigs, surface);
}
+ static SurfaceOutputConfig create(int id, @NonNull Surface surface) {
+ return create(id, -1, null, Collections.emptyList(), surface);
+ }
+
/**
* Get the {@link Surface}. It'll return a valid surface only when type is TYPE_SURFACE.
*/
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java
new file mode 100644
index 0000000..c8e1ef0
--- /dev/null
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/YuvToJpegConverter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.internal.sessionprocessor;
+
+import android.graphics.ImageFormat;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageProcessingUtil;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * A image converter for YUV_420_888 to JPEG. The converted JPEG images were written to the given
+ * output surface after {@link #writeYuvImage(ImageProxy)} is invoked.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class YuvToJpegConverter {
+ private static final String TAG = "YuvToJpegConverter";
+ private final Surface mOutputJpegSurface;
+ @IntRange(from = 1, to = 100)
+ private volatile int mJpegQuality;
+ @ImageOutputConfig.RotationDegreesValue
+ private volatile int mRotationDegrees = 0;
+
+ YuvToJpegConverter(int jpegQuality, @NonNull Surface outputJpegSurface) {
+ mJpegQuality = jpegQuality;
+ mOutputJpegSurface = outputJpegSurface;
+ }
+
+ public void setRotationDegrees(@ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
+ mRotationDegrees = rotationDegrees;
+ }
+
+ void setJpegQuality(int jpgQuality) {
+ mJpegQuality = jpgQuality;
+ }
+
+ static class ConversionFailedException extends Exception {
+ ConversionFailedException(String message) {
+ super(message);
+ }
+ ConversionFailedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * Writes an YUV_420_888 image and converts it into a JPEG image.
+ */
+ void writeYuvImage(@NonNull ImageProxy imageProxy) throws ConversionFailedException {
+ Preconditions.checkState(imageProxy.getFormat() == ImageFormat.YUV_420_888,
+ "Input image is not expected YUV_420_888 image format");
+ try {
+ // TODO(b/258667618): remove extra copy by writing the jpeg data directly into the
+ // Surface's ByteBuffer.
+ boolean success = ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(
+ imageProxy,
+ mJpegQuality,
+ mRotationDegrees,
+ mOutputJpegSurface);
+ if (!success) {
+ throw new ConversionFailedException("Failed to process YUV -> JPEG");
+ }
+ } catch (Exception e) {
+ Logger.e(TAG, "Failed to process YUV -> JPEG", e);
+ throw new ConversionFailedException("Failed to process YUV -> JPEG", e);
+ } finally {
+ imageProxy.close();
+ }
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/Camera2Util.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/Camera2Util.kt
new file mode 100644
index 0000000..cbee160
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/Camera2Util.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.TotalCaptureResult
+import android.os.Handler
+import android.os.Looper
+import android.view.Surface
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import kotlinx.coroutines.CompletableDeferred
+
+/**
+ * Convenient suspend functions for invoking camera2 APIs.
+ */
+@RequiresApi(21)
+object Camera2Util {
+ /**
+ * Open the camera device and return the [CameraDevice] instance.
+ */
+ @DoNotInline
+ suspend fun openCameraDevice(
+ cameraManager: CameraManager,
+ cameraId: String,
+ handler: Handler
+ ): CameraDevice {
+ val deferred = CompletableDeferred<CameraDevice>()
+ cameraManager.openCamera(
+ cameraId,
+ object : CameraDevice.StateCallback() {
+ override fun onOpened(cameraDevice: CameraDevice) {
+ deferred.complete(cameraDevice)
+ }
+
+ override fun onDisconnected(cameraDevice: CameraDevice) {
+ deferred.completeExceptionally(RuntimeException("Camera Disconnected"))
+ }
+
+ override fun onError(cameraDevice: CameraDevice, error: Int) {
+ deferred.completeExceptionally(
+ RuntimeException("Camera onError(error=$cameraDevice)")
+ )
+ }
+ }, handler
+ )
+ return deferred.await()
+ }
+
+ /**
+ * Creates and returns a configured [CameraCaptureSession].
+ */
+ @RequiresApi(21)
+ suspend fun openCaptureSession(
+ cameraDevice: CameraDevice,
+ surfaceList: List<Surface>,
+ handler: Handler
+ ): CameraCaptureSession {
+ val deferred = CompletableDeferred<CameraCaptureSession>()
+ @Suppress("deprecation")
+ cameraDevice.createCaptureSession(
+ surfaceList,
+ object : CameraCaptureSession.StateCallback() {
+
+ override fun onConfigured(session: CameraCaptureSession) {
+ deferred.complete(session)
+ }
+
+ override fun onConfigureFailed(session: CameraCaptureSession) {
+ deferred.completeExceptionally(RuntimeException("onConfigureFailed"))
+ }
+ },
+ handler
+ )
+ return deferred.await()
+ }
+
+ /**
+ * Submits a single capture request to the [CameraCaptureSession] and returns the
+ * [TotalCaptureResult].
+ */
+ suspend fun submitSingleRequest(
+ cameraDevice: CameraDevice,
+ session: CameraCaptureSession,
+ surfaces: List<Surface>,
+ handler: Handler
+ ): TotalCaptureResult {
+ val builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ for (surface in surfaces) {
+ builder.addTarget(surface)
+ }
+ val deferredCapture = CompletableDeferred<TotalCaptureResult>()
+ session.capture(builder.build(), object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ deferredCapture.complete(result)
+ }
+
+ override fun onCaptureFailed(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ failure: CaptureFailure
+ ) {
+ deferredCapture.completeExceptionally(RuntimeException("capture failed"))
+ }
+ }, handler)
+ return deferredCapture.await()
+ }
+
+ /**
+ * Starts the repeating request, and invokes the given block when [TotalCaptureResult] arrives.
+ */
+ fun startRepeating(
+ cameraDevice: CameraDevice,
+ session: CameraCaptureSession,
+ surfaces: List<Surface>,
+ blockForCaptureResult: (TotalCaptureResult) -> Unit
+ ) {
+ val builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+ for (surface in surfaces) {
+ builder.addTarget(surface)
+ }
+ val deferredCapture = CompletableDeferred<TotalCaptureResult>()
+ session.setRepeatingRequest(
+ builder.build(),
+ object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ blockForCaptureResult.invoke(result)
+ }
+
+ override fun onCaptureFailed(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ failure: CaptureFailure
+ ) {
+ deferredCapture.completeExceptionally(RuntimeException("capture failed"))
+ }
+ },
+ Handler(Looper.getMainLooper())
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
index 8aa0f5c..8387b97 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
@@ -30,6 +30,7 @@
import android.view.TextureView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
@@ -132,6 +133,21 @@
*/
@NonNull
public static Preview.SurfaceProvider createAutoDrainingSurfaceTextureProvider() {
+ return createAutoDrainingSurfaceTextureProvider(null);
+ }
+
+ /**
+ * Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture}.
+ *
+ * <p>This method also creates a backing OpenGL thread that will automatically drain frames
+ * from the SurfaceTexture as they become available.
+ *
+ * @param frameAvailableListener listener to be invoked when frame is updated.
+ */
+ @NonNull
+ public static Preview.SurfaceProvider createAutoDrainingSurfaceTextureProvider(
+ @Nullable SurfaceTexture.OnFrameAvailableListener frameAvailableListener
+ ) {
return (surfaceRequest) -> {
HandlerThread handlerThread = new HandlerThread(String.format("CameraX"
+ "-AutoDrainThread-%x", surfaceRequest.hashCode()));
@@ -147,8 +163,12 @@
SurfaceTexture surfaceTexture = new SurfaceTexture(textureIds[0]);
surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
surfaceRequest.getResolution().getHeight());
- surfaceTexture.setOnFrameAvailableListener(
- SurfaceTexture::updateTexImage, handler);
+ surfaceTexture.setOnFrameAvailableListener((st) -> {
+ st.updateTexImage();
+ if (frameAvailableListener != null) {
+ frameAvailableListener.onFrameAvailable(st);
+ }
+ }, handler);
Surface surface = new Surface(surfaceTexture);
surfaceRequest.provideSurface(surface,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index c2a9eef..8d803f1 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -25,6 +25,8 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraConfig;
+import androidx.camera.core.impl.CameraConfigs;
import androidx.camera.core.impl.CameraControlInternal;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
@@ -69,6 +71,8 @@
private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
+ private CameraConfig mCameraConfig = CameraConfigs.emptyConfig();
+
public FakeCamera() {
this(DEFAULT_CAMERA_ID, /*cameraControl=*/null,
new FakeCameraInfoInternal(DEFAULT_CAMERA_ID));
@@ -375,4 +379,15 @@
// notifySurfaceDetached calls.
mConfiguredDeferrableSurfaces.clear();
}
+
+ @NonNull
+ @Override
+ public CameraConfig getExtendedConfig() {
+ return mCameraConfig;
+ }
+
+ @Override
+ public void setExtendedConfig(@Nullable CameraConfig cameraConfig) {
+ mCameraConfig = cameraConfig;
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
index 2c5dec8..e08f64e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
@@ -17,18 +17,19 @@
package androidx.camera.testing.fakes
import android.hardware.camera2.CameraDevice
-import android.media.ImageReader
+import android.hardware.camera2.CaptureRequest
import android.media.ImageWriter
-import android.os.Handler
-import android.os.Looper
import android.os.SystemClock
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.core.CameraInfo
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageReaderProxys
import androidx.camera.core.impl.CameraCaptureFailure
import androidx.camera.core.impl.CameraCaptureResult
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImageReaderProxy
import androidx.camera.core.impl.OptionsBundle
import androidx.camera.core.impl.OutputSurface
import androidx.camera.core.impl.RequestProcessor
@@ -43,20 +44,21 @@
const val FAKE_CAPTURE_SEQUENCE_ID = 1
-@RequiresApi(23)
+@RequiresApi(28) // writing to PRIVATE surface requires API 28+
class FakeSessionProcessor(
val inputFormatPreview: Int?,
val inputFormatCapture: Int?
) : SessionProcessor {
private lateinit var previewProcessorSurface: DeferrableSurface
private lateinit var captureProcessorSurface: DeferrableSurface
- private var intermediaPreviewImageReader: ImageReader? = null
- private var intermediaCaptureImageReader: ImageReader? = null
+ private var imageAnalysisProcessorSurface: DeferrableSurface? = null
+ private var intermediaPreviewImageReader: ImageReaderProxy? = null
+ private var intermediaCaptureImageReader: ImageReaderProxy? = null
private var intermediaPreviewImageWriter: ImageWriter? = null
- private var intermediaCaptureImageWriter: ImageWriter? = null
private val previewOutputConfigId = 1
private val captureOutputConfigId = 2
+ private val analysisOutputConfigId = 3
private var requestProcessor: RequestProcessor? = null
@@ -68,9 +70,13 @@
private val startRepeatingCalled = CompletableDeferred<Long>()
private val startCaptureCalled = CompletableDeferred<Long>()
private val setParametersCalled = CompletableDeferred<Config>()
+ private val startTriggerCalled = CompletableDeferred<Config>()
private var latestParameters: Config = OptionsBundle.emptyBundle()
private var blockRunAfterInitSession: () -> Unit = {}
+ private var rotationDegrees = 0
+ private var jpegQuality = 100
+
fun releaseSurfaces() {
intermediaPreviewImageReader?.close()
intermediaCaptureImageReader?.close()
@@ -87,20 +93,18 @@
imageAnalysisSurfaceConfig: OutputSurface?
): SessionConfig {
initSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
- val handler = Handler(Looper.getMainLooper())
-
- var sessionBuilder = SessionConfig.Builder()
+ val sessionBuilder = SessionConfig.Builder()
// Preview
lateinit var previewTransformedSurface: Surface
if (inputFormatPreview == null) { // no conversion, use origin surface.
previewTransformedSurface = previewSurfaceConfig.surface
} else {
- intermediaPreviewImageReader = ImageReader.newInstance(
- 640, 480,
+ intermediaPreviewImageReader = ImageReaderProxys.createIsolatedReader(
+ previewSurfaceConfig.size.width, previewSurfaceConfig.size.height,
inputFormatPreview, 2
)
- previewTransformedSurface = intermediaPreviewImageReader!!.surface
+ previewTransformedSurface = intermediaPreviewImageReader!!.surface!!
intermediaPreviewImageWriter = ImageWriter.newInstance(
previewSurfaceConfig.surface, 2
@@ -113,7 +117,7 @@
intermediaPreviewImageWriter!!.queueInputImage(imageDequeued)
}
},
- handler
+ CameraXExecutors.ioExecutor()
)
}
previewProcessorSurface =
@@ -132,24 +136,22 @@
if (inputFormatCapture == null) { // no conversion, use origin surface.
captureTransformedSurface = imageCaptureSurfaceConfig.surface
} else {
- intermediaCaptureImageReader = ImageReader.newInstance(
- 640, 480,
+ intermediaCaptureImageReader = ImageReaderProxys.createIsolatedReader(
+ imageCaptureSurfaceConfig.size.width, imageCaptureSurfaceConfig.size.height,
inputFormatCapture, 2
)
- captureTransformedSurface = intermediaCaptureImageReader!!.surface
-
- intermediaCaptureImageWriter = ImageWriter.newInstance(
- imageCaptureSurfaceConfig.surface, 2
- )
+ captureTransformedSurface = intermediaCaptureImageReader!!.surface!!
intermediaCaptureImageReader!!.setOnImageAvailableListener(
{
- it.acquireNextImage().use {
- val imageDequeued = intermediaCaptureImageWriter!!.dequeueInputImage()
- intermediaCaptureImageWriter!!.queueInputImage(imageDequeued)
+ it.acquireNextImage().use { imageProxy ->
+ ImageProcessingUtil.convertYuvToJpegBytesIntoSurface(
+ imageProxy!!, jpegQuality, rotationDegrees,
+ imageCaptureSurfaceConfig.surface
+ )
}
},
- handler
+ CameraXExecutors.ioExecutor()
)
}
captureProcessorSurface =
@@ -158,12 +160,17 @@
captureProcessorSurface.terminationFuture.addListener(
{
intermediaCaptureImageReader?.close()
- intermediaCaptureImageWriter?.close()
},
CameraXExecutors.directExecutor()
)
sessionBuilder.addSurface(captureProcessorSurface)
+ imageAnalysisSurfaceConfig?.let {
+ imageAnalysisProcessorSurface = SessionProcessorSurface(
+ it.surface, analysisOutputConfigId
+ )
+ sessionBuilder.addSurface(imageAnalysisProcessorSurface!!)
+ }
sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
val sessionConfig = sessionBuilder.build()
blockRunAfterInitSession()
@@ -174,11 +181,25 @@
deInitSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
previewProcessorSurface.close()
captureProcessorSurface.close()
+ imageAnalysisProcessorSurface?.close()
}
override fun setParameters(config: Config) {
setParametersCalled.complete(config)
latestParameters = config
+ config.listOptions().filter {
+ it.token is CaptureRequest.Key<*>
+ }.forEach {
+ @Suppress("UNCHECKED_CAST")
+ val key = it.token as CaptureRequest.Key<Any>?
+ if (key == CaptureRequest.JPEG_ORIENTATION) {
+ rotationDegrees = config.retrieveOption(it) as Int
+ }
+
+ if (key == CaptureRequest.JPEG_QUALITY) {
+ jpegQuality = (config.retrieveOption(it) as Byte).toInt()
+ }
+ }
}
override fun onCaptureSessionStart(_requestProcessor: RequestProcessor) {
@@ -299,6 +320,12 @@
return FAKE_CAPTURE_SEQUENCE_ID
}
+ override fun startTrigger(config: Config, callback: SessionProcessor.CaptureCallback): Int {
+ startTriggerCalled.complete(config)
+ callback.onCaptureSequenceCompleted(FAKE_CAPTURE_SEQUENCE_ID)
+ return FAKE_CAPTURE_SEQUENCE_ID
+ }
+
override fun abortCapture(captureSequenceId: Int) {
}
@@ -340,6 +367,10 @@
return setParametersCalled.awaitWithTimeout(3000)
}
+ suspend fun assertStartTriggerInvoked(): Config {
+ return startTriggerCalled.awaitWithTimeout(3000)
+ }
+
private suspend fun <T> Deferred<T>.awaitWithTimeout(timeMillis: Long): T {
return withTimeout(timeMillis) {
await()
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index 1661eea..729cb0a 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -27,7 +27,7 @@
@RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
method public androidx.camera.video.FileDescriptorOutputOptions build();
- method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
}
@@ -39,7 +39,7 @@
@RequiresApi(21) public static final class FileOutputOptions.Builder {
ctor public FileOutputOptions.Builder(java.io.File);
method public androidx.camera.video.FileOutputOptions build();
- method public androidx.camera.video.FileOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
}
@@ -55,13 +55,13 @@
ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
method public androidx.camera.video.MediaStoreOutputOptions build();
method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
- method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public abstract class OutputOptions {
- method @IntRange(from=0) public long getDurationLimit();
+ method @IntRange(from=0) public long getDurationLimitMillis();
method @IntRange(from=0) public long getFileSizeLimit();
method public android.location.Location? getLocation();
field public static final int DURATION_UNLIMITED = 0; // 0x0
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index 1661eea..729cb0a 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -27,7 +27,7 @@
@RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
method public androidx.camera.video.FileDescriptorOutputOptions build();
- method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
}
@@ -39,7 +39,7 @@
@RequiresApi(21) public static final class FileOutputOptions.Builder {
ctor public FileOutputOptions.Builder(java.io.File);
method public androidx.camera.video.FileOutputOptions build();
- method public androidx.camera.video.FileOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
}
@@ -55,13 +55,13 @@
ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
method public androidx.camera.video.MediaStoreOutputOptions build();
method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
- method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public abstract class OutputOptions {
- method @IntRange(from=0) public long getDurationLimit();
+ method @IntRange(from=0) public long getDurationLimitMillis();
method @IntRange(from=0) public long getFileSizeLimit();
method public android.location.Location? getLocation();
field public static final int DURATION_UNLIMITED = 0; // 0x0
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index 1661eea..729cb0a 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -27,7 +27,7 @@
@RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
method public androidx.camera.video.FileDescriptorOutputOptions build();
- method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
}
@@ -39,7 +39,7 @@
@RequiresApi(21) public static final class FileOutputOptions.Builder {
ctor public FileOutputOptions.Builder(java.io.File);
method public androidx.camera.video.FileOutputOptions build();
- method public androidx.camera.video.FileOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
}
@@ -55,13 +55,13 @@
ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
method public androidx.camera.video.MediaStoreOutputOptions build();
method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
- method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
}
@RequiresApi(21) public abstract class OutputOptions {
- method @IntRange(from=0) public long getDurationLimit();
+ method @IntRange(from=0) public long getDurationLimitMillis();
method @IntRange(from=0) public long getFileSizeLimit();
method public android.location.Location? getLocation();
field public static final int DURATION_UNLIMITED = 0; // 0x0
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
index dce6a36..12ef8da 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
@@ -118,12 +118,12 @@
fun canBuildOutputOptions() {
val outputOptions = FakeOutputOptions.Builder()
.setFileSizeLimit(FILE_SIZE_LIMIT)
- .setDurationLimit(DURATION_LIMIT)
+ .setDurationLimitMillis(DURATION_LIMIT)
.build()
assertThat(outputOptions).isNotNull()
assertThat(outputOptions.fileSizeLimit).isEqualTo(FILE_SIZE_LIMIT)
- assertThat(outputOptions.durationLimit).isEqualTo(DURATION_LIMIT)
+ assertThat(outputOptions.durationLimitMillis).isEqualTo(DURATION_LIMIT)
}
@Test
@@ -132,7 +132,7 @@
assertThat(outputOptions.location).isNull()
assertThat(outputOptions.fileSizeLimit).isEqualTo(OutputOptions.FILE_SIZE_UNLIMITED)
- assertThat(outputOptions.durationLimit).isEqualTo(OutputOptions.DURATION_UNLIMITED)
+ assertThat(outputOptions.durationLimitMillis).isEqualTo(OutputOptions.DURATION_UNLIMITED)
}
@Test
@@ -145,7 +145,7 @@
@Test
fun invalidDurationLimit_throwsException() {
assertThrows(IllegalArgumentException::class.java) {
- FakeOutputOptions.Builder().setDurationLimit(INVALID_DURATION_LIMIT)
+ FakeOutputOptions.Builder().setDurationLimitMillis(INVALID_DURATION_LIMIT)
}
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 2f648b8..b7568be 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -597,7 +597,7 @@
val durationTolerance = 50L
val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
val outputOptions = FileOutputOptions.Builder(file)
- .setDurationLimit(durationLimitMs)
+ .setDurationLimitMillis(durationLimitMs)
.build()
val recording = recorder
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
index 127b5a0..c7015d9 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
@@ -53,7 +53,7 @@
/**
* Gets the limit for the file size in bytes.
*
- * @return the file size limit in bytes or zero if it's unlimited.
+ * @return the file size limit in bytes or {@link #FILE_SIZE_UNLIMITED} if it's unlimited.
*/
@IntRange(from = 0)
public long getFileSizeLimit() {
@@ -64,7 +64,7 @@
* Returns a {@link Location} object representing the geographic location where the video was
* recorded.
*
- * @return The location object or {@code null} if no location was set.
+ * @return the location object or {@code null} if no location was set.
*/
@Nullable
public Location getLocation() {
@@ -74,11 +74,12 @@
/**
* Gets the limit for the video duration in milliseconds.
*
- * @return the video duration limit in milliseconds or zero if it's unlimited.
+ * @return the video duration limit in milliseconds or {@link #DURATION_UNLIMITED} if it's
+ * unlimited.
*/
@IntRange(from = 0)
- public long getDurationLimit() {
- return mOutputOptionsInternal.getDurationLimit();
+ public long getDurationLimitMillis() {
+ return mOutputOptionsInternal.getDurationLimitMillis();
}
/**
@@ -93,7 +94,7 @@
mRootInternalBuilder = builder;
// Apply default value
mRootInternalBuilder.setFileSizeLimit(FILE_SIZE_UNLIMITED);
- mRootInternalBuilder.setDurationLimit(DURATION_UNLIMITED);
+ mRootInternalBuilder.setDurationLimitMillis(DURATION_UNLIMITED);
}
/**
@@ -109,12 +110,12 @@
*
* @param fileSizeLimitBytes the file size limit in bytes.
* @return this Builder.
- * @throws IllegalArgumentException if the specified file size limit is less than zero.
+ * @throws IllegalArgumentException if the specified file size limit is negative.
*/
@NonNull
public B setFileSizeLimit(@IntRange(from = 0) long fileSizeLimitBytes) {
Preconditions.checkArgument(fileSizeLimitBytes >= 0, "The specified file size limit "
- + "should be greater than zero.");
+ + "can't be negative.");
mRootInternalBuilder.setFileSizeLimit(fileSizeLimitBytes);
return (B) this;
}
@@ -130,15 +131,15 @@
* unlimited}. If set with a negative value, an {@link IllegalArgumentException} will be
* thrown.
*
- * @param durationLimitMs the video duration limit in milliseconds.
+ * @param durationLimitMillis the video duration limit in milliseconds.
* @return this Builder.
- * @throws IllegalArgumentException if the specified duration limit is less than zero.
+ * @throws IllegalArgumentException if the specified duration limit is negative.
*/
@NonNull
- public B setDurationLimit(@IntRange(from = 0) long durationLimitMs) {
- Preconditions.checkArgument(durationLimitMs >= 0, "The specified duration limit "
- + "should be greater than zero.");
- mRootInternalBuilder.setDurationLimit(durationLimitMs);
+ public B setDurationLimitMillis(@IntRange(from = 0) long durationLimitMillis) {
+ Preconditions.checkArgument(durationLimitMillis >= 0, "The specified duration limit "
+ + "can't be negative.");
+ mRootInternalBuilder.setDurationLimitMillis(durationLimitMillis);
return (B) this;
}
@@ -154,7 +155,8 @@
* value is {@code null}.
*
* @throws IllegalArgumentException if the latitude of the location is not in the range
- * [-90, 90] or the longitude of the location is not in the range [-180, 180].
+ * {@code [-90, 90]} or the longitude of the location is not in the range {@code [-180,
+ * 180]}.
*/
@NonNull
public B setLocation(@Nullable Location location) {
@@ -184,7 +186,7 @@
abstract long getFileSizeLimit();
@IntRange(from = 0)
- abstract long getDurationLimit();
+ abstract long getDurationLimitMillis();
@Nullable
abstract Location getLocation();
@@ -197,7 +199,7 @@
abstract B setFileSizeLimit(@IntRange(from = 0) long fileSizeLimitBytes);
@NonNull
- abstract B setDurationLimit(@IntRange(from = 0) long durationLimitMs);
+ abstract B setDurationLimitMillis(@IntRange(from = 0) long durationLimitMillis);
@NonNull
abstract B setLocation(@Nullable Location location);
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index ad44acd..d36a8bb 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -1472,9 +1472,9 @@
mFileSizeLimitInBytes = OutputOptions.FILE_SIZE_UNLIMITED;
}
- if (recordingToStart.getOutputOptions().getDurationLimit() > 0) {
+ if (recordingToStart.getOutputOptions().getDurationLimitMillis() > 0) {
mDurationLimitNs = TimeUnit.MILLISECONDS.toNanos(
- recordingToStart.getOutputOptions().getDurationLimit());
+ recordingToStart.getOutputOptions().getDurationLimitMillis());
Logger.d(TAG, "Duration limit in nanoseconds: " + mDurationLimitNs);
} else {
mDurationLimitNs = OutputOptions.DURATION_UNLIMITED;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 5f8641f..3b47dfb 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -45,6 +45,7 @@
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
+import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.hardware.camera2.CameraDevice;
@@ -491,6 +492,7 @@
return null;
}
+ @SuppressLint("WrongConstant")
@MainThread
@NonNull
private SessionConfig.Builder createPipeline(@NonNull String cameraId,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index b618ac7..ff30013 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -202,6 +202,7 @@
Long mLastDataStopTimestamp = null;
@SuppressWarnings("WeakerAccess") // synthetic accessor
Future<?> mStopTimeoutFuture = null;
+ private MediaCodecCallback mMediaCodecCallback = null;
private boolean mIsFlushedAfterEndOfStream = false;
private boolean mSourceStoppedSignalled = false;
@@ -282,7 +283,12 @@
mStopTimeoutFuture.cancel(true);
mStopTimeoutFuture = null;
}
- mMediaCodec.setCallback(new MediaCodecCallback());
+ if (mMediaCodecCallback != null) {
+ mMediaCodecCallback.stop();
+ }
+ mMediaCodecCallback = new MediaCodecCallback();
+ mMediaCodec.setCallback(mMediaCodecCallback);
+
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
if (mEncoderInput instanceof SurfaceInput) {
@@ -1014,6 +1020,7 @@
private long mLastSentAdjustedTimeUs = 0L;
private boolean mIsOutputBufferInPauseState = false;
private boolean mIsKeyFrameRequired = false;
+ private boolean mStopped = false;
MediaCodecCallback() {
if (mIsVideoEncoder) {
@@ -1032,6 +1039,10 @@
@Override
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
mEncoderExecutor.execute(() -> {
+ if (mStopped) {
+ Logger.w(mTag, "Receives input frame after codec is reset.");
+ return;
+ }
switch (mState) {
case STARTED:
case PAUSED:
@@ -1057,6 +1068,10 @@
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
@NonNull BufferInfo bufferInfo) {
mEncoderExecutor.execute(() -> {
+ if (mStopped) {
+ Logger.w(mTag, "Receives frame after codec is reset.");
+ return;
+ }
switch (mState) {
case STARTED:
case PAUSED:
@@ -1374,6 +1389,10 @@
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
@NonNull MediaFormat mediaFormat) {
mEncoderExecutor.execute(() -> {
+ if (mStopped) {
+ Logger.w(mTag, "Receives onOutputFormatChanged after codec is reset.");
+ return;
+ }
switch (mState) {
case STARTED:
case PAUSED:
@@ -1404,6 +1423,12 @@
}
});
}
+
+ /** Stop process further frame output. */
+ @ExecutedBy("mEncoderExecutor")
+ void stop() {
+ mStopped = true;
+ }
}
@SuppressWarnings("WeakerAccess") // synthetic accessor
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index f92738c..ce1a726 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -1164,13 +1164,14 @@
* permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
- * @param audioConfig the configuration of audio.
- * @param executor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
+ * @param audioConfig the configuration of audio.
+ * @param executor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the audio config specifies audio should be enabled but the
- * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission
+ * is denied.
*/
@SuppressLint("MissingPermission")
@ExperimentalVideo
@@ -1204,13 +1205,14 @@
* permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
- * @param audioConfig the configuration of audio.
- * @param executor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
+ * @param audioConfig the configuration of audio.
+ * @param executor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the audio config specifies audio should be enabled but the
- * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission
+ * is denied.
*/
@SuppressLint("MissingPermission")
@ExperimentalVideo
@@ -1242,13 +1244,14 @@
* permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
- * @param audioConfig the configuration of audio.
- * @param executor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
+ * @param audioConfig the configuration of audio.
+ * @param executor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the audio config specifies audio should be enabled but the
- * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission
+ * is denied.
*/
@SuppressLint("MissingPermission")
@ExperimentalVideo
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
index 5e26cc6..0f3784f 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
@@ -36,6 +36,7 @@
import androidx.test.espresso.IdlingPolicies
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -102,6 +103,7 @@
}
}
+ @FlakyTest(bugId = 259726932)
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
fun testCameraDisconnect() {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 1781ca6..d13c4c4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -61,16 +61,29 @@
/**
* Waits until an image has been saved and its idling resource has become idle.
+ *
+ * @param captureRequestsCount the capture requests count to issue to continuously take pictures
+ * without waiting for the previous capture requests to be done.
*/
-internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
+internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle(
+ captureRequestsCount: Int = 1
+) {
val idlingResource = withActivity {
cleanTakePictureErrorMessage()
imageSavedIdlingResource
}
try {
- IdlingRegistry.getInstance().register(idlingResource)
// Perform click to take a picture.
- Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(click())
+ Espresso.onView(ViewMatchers.withId(R.id.Picture)).apply {
+ repeat(captureRequestsCount) {
+ perform(click())
+ }
+ }
+ // Registers the idling resource and wait for it being idle after performing the click
+ // operations. So that the click operations can be performed continuously without wait for
+ // previous capture results.
+ IdlingRegistry.getInstance().register(idlingResource)
+ Espresso.onIdle()
} finally { // Always release the idling resource, in case of timeout exceptions.
IdlingRegistry.getInstance().unregister(idlingResource)
withActivity {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 7a17ca1..1d83c85 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -26,7 +26,6 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.location.Location
-import android.media.ImageWriter
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
@@ -34,7 +33,6 @@
import android.util.Size
import android.view.Surface
import androidx.annotation.OptIn
-import androidx.annotation.RequiresApi
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
@@ -51,16 +49,11 @@
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.ViewPort
import androidx.camera.core.impl.CameraConfig
-import androidx.camera.core.impl.CaptureBundle
-import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.CaptureStage
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
import androidx.camera.core.impl.Identifier
import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.core.impl.ImageOutputConfig
-import androidx.camera.core.impl.ImageProxyBundle
import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.utils.CameraOrientationUtil
@@ -70,7 +63,6 @@
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.fakes.FakeCaptureStage
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.camera.testing.fakes.FakeSessionProcessor
import androidx.core.content.ContextCompat
@@ -83,7 +75,6 @@
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
-import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.abs
@@ -632,91 +623,23 @@
}
}
+ @SdkSuppress(minSdkVersion = 28)
@Test(expected = IllegalArgumentException::class)
- fun constructor_withBufferFormatAndCaptureProcessor_throwsException() {
- val captureProcessor = object : CaptureProcessor {
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {}
- override fun process(bundle: ImageProxyBundle) {}
- override fun onResolutionUpdate(size: Size) {}
- }
- ImageCapture.Builder()
+ fun constructor_withBufferFormatAndSessionProcessorIsSet_throwsException(): Unit = runBlocking {
+ val sessionProcessor = FakeSessionProcessor(
+ inputFormatPreview = null, // null means using the same output surface
+ inputFormatCapture = ImageFormat.YUV_420_888
+ )
+
+ val imageCapture = ImageCapture.Builder()
.setBufferFormat(ImageFormat.RAW_SENSOR)
- .setCaptureProcessor(captureProcessor)
.build()
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun constructor_maxCaptureStageInvalid_throwsException() {
- ImageCapture.Builder().setMaxCaptureStages(0).build()
- }
-
- @Test
- fun captureStagesAbove1_withoutCaptureProcessor() = runBlocking {
- val captureBundle = CaptureBundle {
- listOf(
- FakeCaptureStage(0, CaptureConfig.Builder().build()),
- FakeCaptureStage(1, CaptureConfig.Builder().build())
- )
- }
-
- val imageCapture = ImageCapture.Builder()
- .setCaptureBundle(captureBundle)
- .build()
-
+ val preview = Preview.Builder().build()
withContext(Dispatchers.Main) {
- cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
- }
-
- val callback = FakeImageCaptureCallback(capturesCount = 1)
-
- imageCapture.takePicture(mainExecutor, callback)
-
- // Wait for the signal that the image capture has failed.
- callback.awaitCapturesAndAssert(errorsCount = 1)
-
- val error = callback.errors.first()
- assertThat(error).isInstanceOf(ImageCaptureException::class.java)
- assertThat(error.cause).isInstanceOf(IllegalArgumentException::class.java)
- }
-
- @Test
- fun captureStageExceedMaxCaptureStage_whenIssueTakePicture() = runBlocking {
- // Initial the captureStages not greater than the maximum count to bypass the CaptureStage
- // count checking during bindToLifeCycle.
- val captureStages = mutableListOf<CaptureStage>()
- captureStages.add(FakeCaptureStage(0, CaptureConfig.Builder().build()))
-
- val captureBundle = CaptureBundle { captureStages.toList() }
- val captureProcessor = object : CaptureProcessor {
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {}
- override fun process(bundle: ImageProxyBundle) {}
- override fun onResolutionUpdate(size: Size) {}
- }
- val imageCapture = ImageCapture.Builder()
- .setMaxCaptureStages(1)
- .setCaptureBundle(captureBundle)
- .setCaptureProcessor(captureProcessor)
- .build()
-
- withContext(Dispatchers.Main) {
- cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
- }
-
- // Add an additional capture stage to test the case
- // captureStage.size() > mMaxCaptureStages during takePicture.
- captureStages.add(FakeCaptureStage(1, CaptureConfig.Builder().build()))
-
- val callback = FakeImageCaptureCallback(capturesCount = 2)
-
- // Take 2 photos.
- imageCapture.takePicture(mainExecutor, callback)
- imageCapture.takePicture(mainExecutor, callback)
-
- // It should get onError() callback twice.
- callback.awaitCapturesAndAssert(errorsCount = 2)
-
- callback.errors.forEach { error ->
- assertThat(error.cause).isInstanceOf(IllegalArgumentException::class.java)
+ val cameraSelector =
+ getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
+ cameraProvider.bindToLifecycle(
+ fakeLifecycleOwner, cameraSelector, imageCapture, preview)
}
}
@@ -1343,40 +1266,8 @@
assertThat(imageProperties.format).isEqualTo(ImageFormat.YUV_420_888)
}
- // Output JPEG format image when setting a CaptureProcessor is only enabled for devices that
- // API level is at least 29.
@Test
- @SdkSuppress(minSdkVersion = 29)
- fun returnJpegImage_whenCaptureProcessorIsSet() = runBlocking {
- val builder = ImageCapture.Builder()
- val simpleCaptureProcessor = SimpleCaptureProcessor()
-
- // Set a CaptureProcessor to directly pass the image to output surface.
- val useCase = builder.setCaptureProcessor(simpleCaptureProcessor).build()
- var camera: Camera
- withContext(Dispatchers.Main) {
- camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
- }
-
- val callback = FakeImageCaptureCallback(capturesCount = 1)
- useCase.takePicture(mainExecutor, callback)
-
- // Wait for the signal that the image has been captured.
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
- val imageProperties = callback.results.first()
-
- // Check the output image rotation degrees value is correct.
- assertThat(imageProperties.rotationDegrees).isEqualTo(
- camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
- )
- // Check the output format is correct.
- assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
- simpleCaptureProcessor.close()
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
+ @SdkSuppress(minSdkVersion = 28)
fun returnJpegImage_whenSessionProcessorIsSet_outputFormantYuv() = runBlocking {
val builder = ImageCapture.Builder()
val sessionProcessor = FakeSessionProcessor(
@@ -1384,12 +1275,7 @@
inputFormatCapture = ImageFormat.YUV_420_888
)
- val imageCapture = builder
- .setSupportedResolutions(
- listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480))))
- )
- .build()
-
+ val imageCapture = builder.build()
val preview = Preview.Builder().build()
var camera: Camera
@@ -1419,25 +1305,19 @@
}
@Test
- @SdkSuppress(minSdkVersion = 29)
+ @SdkSuppress(minSdkVersion = 28)
fun returnJpegImage_whenSessionProcessorIsSet_outputFormantJpeg() = runBlocking {
assumeFalse(
"Cuttlefish does not correctly handle Jpeg exif. Unable to test.",
Build.MODEL.contains("Cuttlefish")
)
- val builder = ImageCapture.Builder()
val sessionProcessor = FakeSessionProcessor(
inputFormatPreview = null, // null means using the same output surface
inputFormatCapture = null
)
- val imageCapture = builder
- .setSupportedResolutions(
- listOf(android.util.Pair(ImageFormat.JPEG, arrayOf(Size(640, 480))))
- )
- .build()
-
+ val imageCapture = ImageCapture.Builder().build()
val preview = Preview.Builder().build()
withContext(Dispatchers.Main) {
@@ -1507,45 +1387,6 @@
return builder.build()
}
- // Output JPEG format image when setting a CaptureProcessor is only enabled for devices that
- // API level is at least 29.
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun returnJpegImage_whenSoftwareJpegIsEnabledWithCaptureProcessor() = runBlocking {
- val builder = ImageCapture.Builder()
- val simpleCaptureProcessor = SimpleCaptureProcessor()
-
- // Set a CaptureProcessor to directly pass the image to output surface.
- val useCase = builder.setCaptureProcessor(simpleCaptureProcessor).build()
-
- // Enables software Jpeg
- builder.mutableConfig.insertOption(
- ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER,
- true
- )
-
- var camera: Camera
- withContext(Dispatchers.Main) {
- camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
- }
-
- val callback = FakeImageCaptureCallback(capturesCount = 1)
- useCase.takePicture(mainExecutor, callback)
-
- // Wait for the signal that the image has been captured.
- callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
- val imageProperties = callback.results.first()
-
- // Check the output image rotation degrees value is correct.
- assertThat(imageProperties.rotationDegrees).isEqualTo(
- camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
- )
- // Check the output format is correct.
- assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
- simpleCaptureProcessor.close()
- }
-
@kotlin.OptIn(
androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
)
@@ -1706,42 +1547,6 @@
}
}
- private class SimpleCaptureProcessor : CaptureProcessor {
-
- private var imageWriter: ImageWriter? = null
-
- @RequiresApi(Build.VERSION_CODES.M)
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {
- imageWriter = ImageWriter.newInstance(surface, 2)
- }
-
- @RequiresApi(Build.VERSION_CODES.M)
- override fun process(bundle: ImageProxyBundle) {
- val imageProxyListenableFuture = bundle.getImageProxy(bundle.captureIds[0])
- try {
- val imageProxy = imageProxyListenableFuture.get()
- // Directly passing the input YUV image to the output surface.
- imageWriter!!.queueInputImage(imageProxy.image)
- } catch (exception: ExecutionException) {
- throw IllegalArgumentException(
- "Can't extract ImageProxy from the ImageProxyBundle.",
- exception
- )
- } catch (exception: InterruptedException) {
- throw IllegalArgumentException(
- "Can't extract ImageProxy from the ImageProxyBundle.",
- exception
- )
- }
- }
-
- override fun onResolutionUpdate(size: Size) {}
-
- override fun close() {
- imageWriter?.close()
- }
- }
-
private class CountdownDeferred(count: Int) {
private val deferredItems = mutableListOf<CompletableDeferred<Unit>>().apply {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
index a447c7f..578ae88 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
@@ -273,4 +273,24 @@
val randomDelayDuration = (Random.nextInt(maxDelaySeconds) + 1).toLong()
delay(TimeUnit.SECONDS.toMillis(randomDelayDuration))
}
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = LARGE_STRESS_TEST_REPEAT_COUNT)
+ fun launchActivity_thenTakeMultiplePictures_withoutWaitingPreviousResults() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+
+ // Launches CameraXActivity and wait for the preview ready.
+ val activityScenario =
+ launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+ with(activityScenario) {
+ use {
+ // Checks whether multiple images can be captured successfully
+ repeat(STRESS_TEST_OPERATION_REPEAT_COUNT) {
+ takePictureAndWaitForImageSavedIdle(3)
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
index d294dbc..9d7efbd 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
@@ -22,29 +22,19 @@
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.CameraSelector
-import androidx.camera.core.impl.CaptureBundle
-import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.extensions.internal.ExtensionVersion
import androidx.camera.extensions.internal.Version
import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
-import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
-import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
-import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
import androidx.camera.integration.extensions.utils.CameraSelectorUtil
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.CoreAppTestUtil
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.UiDevice
-import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
@@ -188,53 +178,4 @@
// the same.
assertThat(latencyInfo).isEqualTo(expectedLatencyInfo)
}
-
- @LargeTest
- @Test
- fun returnCaptureStages_whenCaptureProcessorIsNotNull(): Unit = runBlocking {
- // Clear the device UI and check if there is no dialog or lock screen on the top of the
- // window before starting the test.
- CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
-
- val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
- setOrientationNatural()
- }
-
- val activityScenario = launchCameraExtensionsActivity(
- config.cameraId,
- config.extensionMode
- )
-
- with(activityScenario) {
- use {
- var captureBundle: CaptureBundle? = null
- withActivity {
- // Retrieves the CaptureProcessor from ImageCapture's config
- val captureProcessor = imageCapture!!.currentConfig.retrieveOption(
- ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR, null
- )
-
- assumeTrue(captureProcessor != null)
-
- // Retrieves the CaptureBundle from ImageCapture's config
- captureBundle = imageCapture!!.currentConfig.retrieveOption(
- ImageCaptureConfig.OPTION_CAPTURE_BUNDLE
- )
- }
-
- waitForPreviewViewStreaming()
-
- // Calls CaptureBundle#getCaptureStages() will call
- // ImageCaptureExtenderImpl#getCaptureStages(). Checks the returned value is
- // not empty.
- assertThat(captureBundle!!.captureStages).isNotEmpty()
- }
- }
-
- // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
- // to other tests :)
- device.unfreezeRotation()
- device.pressHome()
- device.waitForIdle(HOME_TIMEOUT_MS)
- }
}
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
deleted file mode 100644
index 571c3bf..0000000
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.integration.extensions
-
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.graphics.SurfaceTexture.OnFrameAvailableListener
-import android.os.Build
-import android.os.Handler
-import android.os.HandlerThread
-import android.util.Size
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.interop.Camera2CameraInfo
-import androidx.camera.core.CameraFilter
-import androidx.camera.core.CameraInfo
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.Preview
-import androidx.camera.core.impl.CameraConfig
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
-import androidx.camera.core.impl.Identifier
-import androidx.camera.core.impl.MutableOptionsBundle
-import androidx.camera.core.impl.OptionsBundle
-import androidx.camera.core.impl.PreviewConfig
-import androidx.camera.core.impl.UseCaseConfigFactory
-import androidx.camera.extensions.ExtensionMode
-import androidx.camera.extensions.ExtensionsManager
-import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
-import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
-import androidx.camera.integration.extensions.utils.CameraSelectorUtil
-import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.GLUtil
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
-import androidx.camera.testing.TimestampCaptureProcessor
-import androidx.camera.testing.TimestampCaptureProcessor.TimestampListener
-import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import junit.framework.AssertionFailedError
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.After
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeNotNull
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = 21)
-class PreviewProcessorTimestampTest(private val config: CameraIdExtensionModePair) {
- @get:Rule
- val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
- )
-
- private val context = ApplicationProvider.getApplicationContext<Context>()
-
- private lateinit var cameraProvider: ProcessCameraProvider
- private lateinit var extensionsManager: ExtensionsManager
- private lateinit var baseCameraSelector: CameraSelector
- private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
-
- private val inputTimestampsLatch = CountDownLatch(1)
- private val outputTimestampsLatch = CountDownLatch(1)
- private val surfaceTextureLatch = CountDownLatch(1)
- private val inputTimestamps = hashSetOf<Long>()
- private val outputTimestamps = hashSetOf<Long>()
-
- private val timestampListener = object : TimestampListener {
- private var complete = false
-
- override fun onTimestampAvailable(timestamp: Long) {
- if (complete) {
- return
- }
-
- inputTimestamps.add(timestamp)
- if (inputTimestamps.size >= 10) {
- inputTimestampsLatch.countDown()
- complete = true
- }
- }
- }
-
- private var isSurfaceTextureReleased = false
- private val isSurfaceTextureReleasedLock = Any()
-
- private val onFrameAvailableListener = object : OnFrameAvailableListener {
- private var complete = false
-
- override fun onFrameAvailable(surfaceTexture: SurfaceTexture): Unit = runBlocking {
- if (complete) {
- return@runBlocking
- }
-
- withContext(Dispatchers.Main) {
- synchronized(isSurfaceTextureReleasedLock) {
- if (!isSurfaceTextureReleased) {
- surfaceTexture.updateTexImage()
- }
- }
- }
-
- outputTimestamps.add(surfaceTexture.timestamp)
-
- if (outputTimestamps.size >= 10) {
- outputTimestampsLatch.countDown()
- complete = true
- }
- }
- }
-
- private val processingHandler: Handler
- private val processingHandlerThread = HandlerThread("Processing").also {
- it.start()
- processingHandler = Handler(it.looper)
- }
-
- @Before
- fun setUp(): Unit = runBlocking {
- assumeTrue(CameraXExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
- assumeFalse(Build.BRAND.equals("Samsung", ignoreCase = true))
- cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
- extensionsManager = ExtensionsManager.getInstanceAsync(
- context,
- cameraProvider
- )[10000, TimeUnit.MILLISECONDS]
-
- val (cameraId, extensionMode) = config
- baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
- assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
-
- withContext(Dispatchers.Main) {
- fakeLifecycleOwner = FakeLifecycleOwner()
- fakeLifecycleOwner.startAndResume()
- }
- }
-
- @After
- fun cleanUp(): Unit = runBlocking {
- if (::cameraProvider.isInitialized) {
- withContext(Dispatchers.Main) {
- cameraProvider.unbindAll()
- cameraProvider.shutdown()[10000, TimeUnit.MILLISECONDS]
- }
- }
-
- if (::extensionsManager.isInitialized) {
- extensionsManager.shutdown()[10000, TimeUnit.MILLISECONDS]
- }
- }
-
- @Test
- fun timestampIsCorrect(): Unit = runBlocking {
- withContext(Dispatchers.Main) {
- val preview = Preview.Builder().build()
-
- preview.setSurfaceProvider(
- SurfaceTextureProvider.createSurfaceTextureProvider(createSurfaceTextureCallback())
- )
-
- // Retrieves the camera selector which a timestamp capture processor is applied
- val timestampExtensionEnabledCameraSelector =
- getTimestampExtensionEnabledCameraSelector(
- extensionsManager,
- config.extensionMode,
- baseCameraSelector
- )
-
- cameraProvider.bindToLifecycle(
- fakeLifecycleOwner,
- timestampExtensionEnabledCameraSelector,
- preview
- )
- }
-
- // Waits for the surface texture being ready
- assertThat(surfaceTextureLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-
- // Waits for 10 input and output frame timestamps are collected
- assertThat(inputTimestampsLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
- assertThat(outputTimestampsLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
-
- // Verifies that the input and output frame timestamps are the same
- assertThat(outputTimestamps).containsExactlyElementsIn(inputTimestamps)
- }
-
- private fun createSurfaceTextureCallback(): SurfaceTextureCallback =
- object : SurfaceTextureCallback {
- override fun onSurfaceTextureReady(
- surfaceTexture: SurfaceTexture,
- resolution: Size
- ) {
- surfaceTexture.attachToGLContext(GLUtil.getTexIdFromGLContext())
- surfaceTexture.setOnFrameAvailableListener(
- onFrameAvailableListener, processingHandler
- )
- surfaceTextureLatch.countDown()
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- synchronized(isSurfaceTextureReleasedLock) {
- isSurfaceTextureReleased = true
- surfaceTexture.release()
- }
- }
- }
-
- companion object {
- @JvmStatic
- @get:Parameterized.Parameters(name = "config = {0}")
- val parameters: Collection<CameraIdExtensionModePair>
- get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
-
- /**
- * Retrieves the default extended camera config provider id string
- */
- private fun getExtendedCameraConfigProviderId(@ExtensionMode.Mode mode: Int): String =
- when (mode) {
- ExtensionMode.BOKEH -> "EXTENSION_MODE_BOKEH"
- ExtensionMode.HDR -> "EXTENSION_MODE_HDR"
- ExtensionMode.NIGHT -> "EXTENSION_MODE_NIGHT"
- ExtensionMode.FACE_RETOUCH -> "EXTENSION_MODE_FACE_RETOUCH"
- ExtensionMode.AUTO -> "EXTENSION_MODE_AUTO"
- ExtensionMode.NONE -> "EXTENSION_MODE_NONE"
- else -> throw IllegalArgumentException("Invalid extension mode!")
- }.let {
- return ":camera:camera-extensions-$it"
- }
-
- /**
- * Retrieves the timestamp extended camera config provider id string
- */
- private fun getTimestampCameraConfigProviderId(@ExtensionMode.Mode mode: Int): String =
- "${getExtendedCameraConfigProviderId(mode)}-timestamp"
- }
-
- /**
- * Gets the camera selector which a timestamp capture processor is applied
- */
- private fun getTimestampExtensionEnabledCameraSelector(
- extensionsManager: ExtensionsManager,
- extensionMode: Int,
- baseCameraSelector: CameraSelector
- ): CameraSelector {
- // Injects the TimestampExtensionsUseCaseConfigFactory which allows to monitor and verify
- // the frames' timestamps
- injectTimestampExtensionsUseCaseConfigFactory(
- extensionsManager,
- extensionMode,
- baseCameraSelector
- )
-
- val builder = CameraSelector.Builder.fromSelector(baseCameraSelector)
- // Add a TimestampExtensionCameraFilter which includes the CameraFilter to check whether
- // the camera is supported for the extension mode or not and also includes the identifier
- // to find the extended camera config provider from ExtendedCameraConfigProviderStore
- builder.addCameraFilter(TimestampExtensionCameraFilter(extensionsManager, extensionMode))
- return builder.build()
- }
-
- /**
- * Injects the TimestampExtensionsUseCaseConfigFactory which allows to monitor and verify the
- * frames' timestamps
- */
- private fun injectTimestampExtensionsUseCaseConfigFactory(
- extensionsManager: ExtensionsManager,
- extensionMode: Int,
- baseCameraSelector: CameraSelector
- ): Unit = runBlocking {
- val timestampConfigProviderId =
- Identifier.create(getTimestampCameraConfigProviderId(extensionMode))
-
- // Calling the ExtensionsManager#getExtensionEnabledCameraSelector() function to add the
- // default extended camera config provider to ExtendedCameraConfigProviderStore
- extensionsManager.getExtensionEnabledCameraSelector(baseCameraSelector, extensionMode)
-
- ExtendedCameraConfigProviderStore.addConfig(timestampConfigProviderId) {
- cameraInfo: CameraInfo, context: Context ->
-
- // Retrieves the default extended camera config provider and
- // ExtensionsUseCaseConfigFactory
- val defaultConfigProviderId =
- Identifier.create(getExtendedCameraConfigProviderId(extensionMode))
- val defaultCameraConfigProvider =
- ExtendedCameraConfigProviderStore.getConfigProvider(defaultConfigProviderId)
- val defaultCameraConfig = defaultCameraConfigProvider.getConfig(cameraInfo, context)!!
- val defaultExtensionsUseCaseConfigFactory =
- defaultCameraConfig.retrieveOption(CameraConfig.OPTION_USECASE_CONFIG_FACTORY)
-
- // Creates a new TimestampExtensionsUseCaseConfigFactory on top of the default
- // ExtensionsUseCaseConfigFactory to monitor the frames' timestamps
- val timestampExtensionsUseCaseConfigFactory = TimestampExtensionsUseCaseConfigFactory(
- defaultExtensionsUseCaseConfigFactory!!,
- timestampListener
- )
-
- // Creates the config from the original config and replaces its use case config factory
- // with the TimestampExtensionsUseCaseConfigFactory
- val mutableOptionsBundle = MutableOptionsBundle.from(defaultCameraConfig)
- mutableOptionsBundle.insertOption(
- CameraConfig.OPTION_USECASE_CONFIG_FACTORY,
- timestampExtensionsUseCaseConfigFactory
- )
-
- // Returns a CameraConfig implemented with the updated config
- object : CameraConfig {
- val config = OptionsBundle.from(mutableOptionsBundle)
-
- override fun getConfig(): Config {
- return config
- }
-
- override fun getCompatibilityId(): Identifier {
- return config.retrieveOption(CameraConfig.OPTION_COMPATIBILITY_ID)!!
- }
- }
- }
- }
-
- /**
- * A TimestampExtensionCameraFilter which includes the CameraFilter to check whether the camera
- * is supported for the extension mode or not and also includes the identifier to find the
- * extended camera config provider from ExtendedCameraConfigProviderStore.
- */
- private class TimestampExtensionCameraFilter constructor(
- private val extensionManager: ExtensionsManager,
- @ExtensionMode.Mode private val mode: Int
- ) : CameraFilter {
- override fun getIdentifier(): Identifier {
- return Identifier.create(getTimestampCameraConfigProviderId(mode))
- }
-
- override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
- val resultInfos = mutableListOf<CameraInfo>()
-
- cameraInfos.forEach {
- val cameraId = Camera2CameraInfo.from(it).cameraId
- val cameraIdCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
- if (extensionManager.isExtensionAvailable(cameraIdCameraSelector, mode)) {
- resultInfos.add(it)
- }
- }
-
- return resultInfos
- }
- }
-
- /**
- * A UseCaseConfigFactory implemented on top of the default ExtensionsUseCaseConfigFactory to
- * monitor the frames' timestamps
- */
- private class TimestampExtensionsUseCaseConfigFactory constructor(
- private val useCaseConfigFactory: UseCaseConfigFactory,
- private val timestampListener: TimestampListener
- ) :
- UseCaseConfigFactory {
- override fun getConfig(
- captureType: UseCaseConfigFactory.CaptureType,
- captureMode: Int
- ): Config? {
- // Retrieves the config from the default ExtensionsUseCaseConfigFactory
- val mutableOptionsBundle = useCaseConfigFactory.getConfig(
- captureType, captureMode)?.let {
- MutableOptionsBundle.from(it)
- } ?: throw AssertionFailedError("Can not retrieve config for capture type $captureType")
-
- // Replaces the PreviewCaptureProcessor by the TimestampCaptureProcessor to monitor the
- // frames' timestamps
- if (captureType.equals(UseCaseConfigFactory.CaptureType.PREVIEW)) {
- val previewCaptureProcessor = mutableOptionsBundle.retrieveOption(
- PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR,
- null
- )
-
- assumeNotNull(previewCaptureProcessor)
-
- mutableOptionsBundle.insertOption(
- PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR,
- TimestampCaptureProcessor(previewCaptureProcessor!!, timestampListener)
- )
- }
-
- return OptionsBundle.from(mutableOptionsBundle)
- }
- }
-}
\ No newline at end of file
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 306320a..0497df5 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -482,6 +482,15 @@
}
+package androidx.car.app.mediaextensions {
+
+ public final class MetadataExtras {
+ field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+ }
+
+}
+
package androidx.car.app.model {
@androidx.car.app.annotations.CarProtocol public final class Action {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 5bc3d2c..9fb0b45 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -844,6 +844,15 @@
}
+package androidx.car.app.mediaextensions {
+
+ public final class MetadataExtras {
+ field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+ }
+
+}
+
package androidx.car.app.messaging {
@androidx.car.app.annotations.ExperimentalCarApi public class MessagingServiceConstants {
@@ -1405,7 +1414,7 @@
}
@androidx.car.app.annotations.CarProtocol public final class Row implements androidx.car.app.model.Item {
- method @androidx.car.app.annotations.ExperimentalCarApi public java.util.List<androidx.car.app.model.Action!> getActions();
+ method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public java.util.List<androidx.car.app.model.Action!> getActions();
method public androidx.car.app.model.CarIcon? getImage();
method public androidx.car.app.model.Metadata? getMetadata();
method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public int getNumericDecoration();
@@ -1426,7 +1435,7 @@
public static final class Row.Builder {
ctor public Row.Builder();
- method @androidx.car.app.annotations.ExperimentalCarApi public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
+ method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public androidx.car.app.model.Row.Builder addAction(androidx.car.app.model.Action);
method public androidx.car.app.model.Row.Builder addText(CharSequence);
method public androidx.car.app.model.Row.Builder addText(androidx.car.app.model.CarText);
method public androidx.car.app.model.Row build();
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 306320a..0497df5 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -482,6 +482,15 @@
}
+package androidx.car.app.mediaextensions {
+
+ public final class MetadataExtras {
+ field public static final String KEY_DESCRIPTION_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+ field public static final String KEY_SUBTITLE_LINK_MEDIA_ID = "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+ }
+
+}
+
package androidx.car.app.model {
@androidx.car.app.annotations.CarProtocol public final class Action {
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
new file mode 100644
index 0000000..1ad83f2
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MetadataExtras.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2022 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.car.app.mediaextensions;
+
+import android.os.Bundle;
+
+/**
+ * Defines constants for extra keys in {@link android.support.v4.media.MediaMetadataCompat} or
+ * {@link androidx.media3.common.MediaMetadata}.
+ */
+public final class MetadataExtras {
+
+ // Do not instantiate
+ private MetadataExtras() {
+ }
+
+ /**
+ * {@link Bundle} key used in the extras of a media item to indicate that the subtitle of the
+ * corresponding media item can be linked to another media item ID.
+ * <p>The value of the extra is set to the media ID of this other item.
+ *
+ * <p>NOTE: media1 and media3 apps setting this extra <b>must implement</b>
+ * {@link androidx.media.MediaBrowserServiceCompat#onLoadItem} or
+ * {@link androidx.media3.session.MediaLibraryService.Callback#onGetItem} respectively.
+ * <p>NOTE: media apps setting this extra <b>must explicitly set the subtitle property</b>.
+ * <p>See {@link android.support.v4.media.MediaMetadataCompat#METADATA_KEY_DISPLAY_SUBTITLE}
+ * <p>See {@link androidx.media3.common.MediaMetadata#subtitle}
+ *
+ * <p>TYPE: String.
+ * <p>
+ * <p> Example:
+ * <pre>
+ * "Source" MediaItem
+ * + mediaId: “Beethoven-9th-symphony” // ID
+ * + title: “9th symphony” // Track
+ * + subtitle: “The best of Beethoven” // Album
+ * ╔════+ subtitleLinkMediaId: “Beethoven-best-of” // Album ID
+ * ║ + description: “Beethoven” // Artist
+ * ║ + descriptionLinkMediaId: “artist:Beethoven” // Artist ID
+ * ║
+ * ║ "Destination" MediaItem
+ * ╚════+ mediaId: “Beethoven-best-of” // ID
+ * + title: “The best of Beethoven” // Album
+ * + subtitle: “Beethoven” // Artist
+ * + subtitleLinkMediaId: “artist:Beethoven” // Artist ID
+ * </pre>
+ **/
+ public static final String KEY_SUBTITLE_LINK_MEDIA_ID =
+ "androidx.car.app.mediaextensions.KEY_SUBTITLE_LINK_MEDIA_ID";
+
+ /**
+ * {@link Bundle} key used in the extras of a media item to indicate that the description of the
+ * corresponding media item can be linked to another media item ID.
+ * <p>The value of the extra is set to the media ID of this other item.
+ *
+ * <p>NOTE: media1 and media3 apps setting this extra <b>must implement</b>
+ * {@link androidx.media.MediaBrowserServiceCompat#onLoadItem} or
+ * {@link androidx.media3.session.MediaLibraryService.Callback#onGetItem} respectively.
+ * <p>NOTE: media apps setting this extra <b>must explicitly set the description property</b>.
+ * <p>See {@link android.support.v4.media.MediaMetadataCompat#METADATA_KEY_DISPLAY_DESCRIPTION}
+ * <p>See {@link androidx.media3.common.MediaMetadata#description}
+ *
+ * <p>TYPE: String.
+ * <p>
+ * <p> Example:
+ * <pre>
+ * "Source" MediaItem
+ * + mediaId: “Beethoven-9th-symphony” // ID
+ * + title: “9th symphony” // Track
+ * + subtitle: “The best of Beethoven” // Album
+ * + subtitleLinkMediaId: “Beethoven-best-of” // Album ID
+ * + description: “Beethoven” // Artist
+ * ╔════+ descriptionLinkMediaId: “artist:Beethoven” // Artist ID
+ * ║
+ * ║ "Destination" MediaItem
+ * ╚════+ mediaId: “artist:Beethoven” // ID
+ * + title: “Beethoven” // Artist
+ * </pre>
+ **/
+ public static final String KEY_DESCRIPTION_LINK_MEDIA_ID =
+ "androidx.car.app.mediaextensions.KEY_DESCRIPTION_LINK_MEDIA_ID";
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/package-info.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/package-info.java
new file mode 100644
index 0000000..c86a9d5
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 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.
+ */
+
+/**
+ * The mediaextensions package defines car specific extensions to the
+ * {@link android.support.v4.media}, {@link androidx.media} and {@link androidx.media3} libraries.
+ */
+package androidx.car.app.mediaextensions;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index e278047..955c258b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -165,6 +165,7 @@
*/
@ExperimentalCarApi
@NonNull
+ @RequiresCarApi(6)
public List<Action> getActions() {
return mActions;
}
@@ -540,6 +541,7 @@
*/
@ExperimentalCarApi
@NonNull
+ @RequiresCarApi(6)
public Builder addAction(@NonNull Action action) {
List<Action> mActionsCopy = new ArrayList<>(mActions);
mActionsCopy.add(requireNonNull(action));
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 696229d..4ff282e 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -144,11 +144,3 @@
android {
namespace "androidx.compose.animation.core"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all"
- ]
- }
-}
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index 5d96f14..1e1ab09 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -131,11 +131,3 @@
android {
namespace "androidx.compose.animation.graphics"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/animation/animation-tooling-internal/build.gradle b/compose/animation/animation-tooling-internal/build.gradle
index 084c4ff..87aeae3 100644
--- a/compose/animation/animation-tooling-internal/build.gradle
+++ b/compose/animation/animation-tooling-internal/build.gradle
@@ -27,14 +27,6 @@
implementation(libs.kotlinStdlib)
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
-
androidx {
name = "Compose Animation Tooling"
description = "Compose Animation APIs for tooling support. Internal use only."
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 229e0fc..cc0c1c7 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -130,11 +130,3 @@
android {
namespace "androidx.compose.animation"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
index f809ad9..3038c88 100644
--- a/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
+++ b/compose/compiler/compiler-daemon/src/main/kotlin/androidx/compose/compiler/daemon/Compiler.kt
@@ -58,6 +58,7 @@
}
}
+@JvmDefaultWithCompatibility
interface DaemonCompiler {
fun compile(
args: Array<String>,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 26f1440..c421acc 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -1626,4 +1626,69 @@
}
"""
)
+
+ @Test
+ fun testForEarlyExit() = verifyComposeIrTransform(
+ source = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ val value = remember { mutableStateOf(false) }
+ if (!value.value && !condition) return
+ val value2 = remember { mutableStateOf(false) }
+ Text("Text ${'$'}{value.value}, ${'$'}{value2.value}")
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<rememb...>,<Text("...>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val value = %composer.cache(false) {
+ mutableStateOf(
+ value = false
+ )
+ }
+ if (!value.value && !condition) {
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ val value2 = remember({
+ mutableStateOf(
+ value = false
+ )
+ }, %composer, 0)
+ Text("Text %{value.value}, %{value2.value}", %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """,
+ extra = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Text(value: String) { }
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt
index c11e7ee..0c868f1 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/BuildMetrics.kt
@@ -38,6 +38,7 @@
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtParameter
+@JvmDefaultWithCompatibility
interface FunctionMetrics {
val isEmpty: Boolean get() = false
val packageName: FqName
@@ -81,6 +82,7 @@
fun print(out: Appendable, src: IrSourcePrinterVisitor)
}
+@JvmDefaultWithCompatibility
interface ModuleMetrics {
val isEmpty get() = false
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 33fecf3f8..2c3f561 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -590,9 +590,6 @@
protected fun IrType.binaryOperator(name: Name, paramType: IrType): IrFunctionSymbol =
context.symbols.getBinaryOperator(name, this, paramType)
- protected fun IrType.unaryOperator(name: Name): IrFunctionSymbol =
- context.symbols.getUnaryOperator(name, this)
-
protected fun irAnd(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
return irCall(
lhs.type.binaryOperator(OperatorNameConventions.AND, rhs.type),
@@ -603,15 +600,6 @@
)
}
- protected fun irInv(lhs: IrExpression): IrCallImpl {
- val int = context.irBuiltIns.intType
- return irCall(
- int.unaryOperator(OperatorNameConventions.INV),
- null,
- lhs
- )
- }
-
protected fun irOr(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
val int = context.irBuiltIns.intType
return irCall(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 3541e35..a3f26b6 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -287,6 +287,7 @@
changedParamCount(realValueParams, thisParams)
}
+@JvmDefaultWithCompatibility
interface IrChangedBitMaskValue {
val used: Boolean
val declarations: List<IrValueDeclaration>
@@ -313,6 +314,7 @@
fun putAsValueArgumentIn(fn: IrFunctionAccessExpression, startIndex: Int)
}
+@JvmDefaultWithCompatibility
interface IrChangedBitMaskVariable : IrChangedBitMaskValue {
fun asStatements(): List<IrStatement>
fun irOrSetBitsAtSlot(slot: Int, value: IrExpression): IrExpression
@@ -2627,7 +2629,7 @@
}
}
}
-
+ scope.updateIntrinsiceRememberSafety(false)
break@loop
}
if (scope.isInlinedLambda && scope.inComposableCall)
@@ -4544,7 +4546,7 @@
temp,
irAnd(
irGet(temp),
- irInv(irConst(ParamState.Mask.bitsForSlot(slot)))
+ irConst(ParamState.Mask.bitsForSlot(slot).inv())
)
)
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index b535bdc..5c22d57 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -17,6 +17,7 @@
package androidx.compose.compiler.plugins.kotlin.lower
import androidx.compose.compiler.plugins.kotlin.KtxNameConventions
+import java.util.Locale
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.IrBuiltIns
@@ -98,6 +99,7 @@
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.isAny
import org.jetbrains.kotlin.ir.types.isInt
+import org.jetbrains.kotlin.ir.types.isMarkedNullable
import org.jetbrains.kotlin.ir.types.isNullableAny
import org.jetbrains.kotlin.ir.types.isUnit
import org.jetbrains.kotlin.ir.util.isAnnotationClass
@@ -110,9 +112,6 @@
import org.jetbrains.kotlin.resolve.descriptorUtil.isAnnotationConstructor
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.utils.Printer
-import java.util.Locale
-import kotlin.math.abs
-import org.jetbrains.kotlin.ir.types.isMarkedNullable
fun IrElement.dumpSrc(): String {
val sb = StringBuilder()
@@ -999,14 +998,14 @@
private fun intAsBinaryString(value: Int): String {
if (value == 0) return "0"
- var current = abs(value)
+ var current = if (value >= 0) value else value.inv()
var result = ""
while (current != 0 || result.length % 4 != 0) {
val nextBit = current and 1 != 0
- current = current shr 1
+ current = current ushr 1
result = "${if (nextBit) "1" else "0"}$result"
}
- return "${if (value < 0) "-" else ""}0b$result"
+ return "0b$result" + if (value < 0) ".inv()" else ""
}
override fun visitConst(expression: IrConst<*>) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
index 915066b..9c6fce4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
@@ -50,6 +50,7 @@
import org.jetbrains.kotlin.ir.util.module
import org.jetbrains.kotlin.ir.util.remapTypeParameters
+@JvmDefaultWithCompatibility
internal interface DecoyTransformBase {
val context: IrPluginContext
val signatureBuilder: IdSignatureSerializer
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 4ce01dd..2385d19 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -139,11 +139,3 @@
android {
namespace "androidx.compose.foundation.layout"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all"
- ]
- }
-}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 05e0a32..a01aa33 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -157,11 +157,8 @@
method public suspend Object? consumePreFling(long velocity, kotlin.coroutines.Continuation<? super androidx.compose.ui.unit.Velocity>);
method public long consumePreScroll(long scrollDelta, int source);
method public androidx.compose.ui.Modifier getEffectModifier();
- method public boolean isEnabled();
method public boolean isInProgress();
- method public void setEnabled(boolean);
property public abstract androidx.compose.ui.Modifier effectModifier;
- property public abstract boolean isEnabled;
property public abstract boolean isInProgress;
}
@@ -912,6 +909,7 @@
@androidx.compose.foundation.ExperimentalFoundationApi public sealed interface LazyStaggeredGridLayoutInfo {
method public int getAfterContentPadding();
method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
method public int getTotalItemsCount();
method public int getViewportEndOffset();
method public long getViewportSize();
@@ -919,6 +917,7 @@
method public java.util.List<androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo> getVisibleItemsInfo();
property public abstract int afterContentPadding;
property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
property public abstract int totalItemsCount;
property public abstract int viewportEndOffset;
property public abstract long viewportSize;
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
index 82f9b23..e8c54a3 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.benchmark.lazy.MotionEventHelper
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -32,6 +33,7 @@
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.testutils.LayeredComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
@@ -123,11 +125,19 @@
view = LocalView.current
if (!::motionEventHelper.isInitialized) motionEventHelper = MotionEventHelper(view)
val scrollState = rememberScrollState()
- val overscrollEffect = ScrollableDefaults.overscrollEffect().apply { isEnabled = true }
+ val wrappedScrollState = remember(scrollState) {
+ object : ScrollableState by scrollState {
+ override val canScrollForward: Boolean
+ get() = true
+ override val canScrollBackward: Boolean
+ get() = true
+ }
+ }
+ val overscrollEffect = ScrollableDefaults.overscrollEffect()
Box(
Modifier
.scrollable(
- scrollState,
+ wrappedScrollState,
orientation = Orientation.Vertical,
reverseDirection = true,
overscrollEffect = overscrollEffect
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index dd28bf30..1988c5c 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -177,11 +177,3 @@
description = "Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers"
legacyDisableKotlinStrictApiMode = true
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EmojiCompatDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EmojiCompatDemo.kt
new file mode 100644
index 0000000..b6995d6
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EmojiCompatDemo.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun EmojiCompatDemo() {
+ val emoji14MeltingFace = "\uD83E\uDEE0"
+ val emoji13WomanFeedingBaby = "\uD83D\uDC69\uD83C\uDFFE\u200D\uD83C\uDF7C"
+ val emoji13DisguisedFace = "\uD83E\uDD78"
+ val emoji12HoldingHands =
+ "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFF"
+ val emoji12Flamingo = "\uD83E\uDDA9"
+ val emoji11PartyingFace = "\uD83E\uDD73"
+
+ val text = "11: $emoji11PartyingFace 12: $emoji12Flamingo $emoji12HoldingHands " +
+ "13: $emoji13DisguisedFace $emoji13WomanFeedingBaby " +
+ "14: $emoji14MeltingFace"
+
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)) {
+ Text(text = text, modifier = Modifier.padding(16.dp))
+
+ val textFieldValue = remember { mutableStateOf(TextFieldValue(text)) }
+
+ TextField(
+ value = textFieldValue.value,
+ modifier = Modifier.padding(16.dp),
+ onValueChange = { textFieldValue.value = it }
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index eb423ca..6562a8e 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -73,6 +73,7 @@
ComposableDemo("Variable Fonts") { VariableFontsDemo() },
ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
ComposableDemo("All system font families") { SystemFontFamilyDemo() },
+ ComposableDemo("Emoji Compat") { EmojiCompatDemo() },
)
),
DemoCategory(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
index 02c04af..072596d 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
@@ -69,7 +69,7 @@
// relaxation: when we are in progress of the overscroll and user scrolls in the
// different direction = substract the overscroll first
val sameDirection = sign(scrollDelta.y) == sign(overscrollOffset.value)
- return if (abs(overscrollOffset.value) > 0.5 && !sameDirection && isEnabled) {
+ return if (abs(overscrollOffset.value) > 0.5 && !sameDirection) {
val prevOverscrollValue = overscrollOffset.value
val newOverscrollValue = overscrollOffset.value + scrollDelta.y
if (sign(prevOverscrollValue) != sign(newOverscrollValue)) {
@@ -93,7 +93,7 @@
source: NestedScrollSource
) {
// if it is a drag, not a fling, add the delta left to our over scroll value
- if (abs(overscrollDelta.y) > 0.5 && isEnabled && source == NestedScrollSource.Drag) {
+ if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.Drag) {
scope.launch {
// multiply by 0.1 for the sake of parallax effect
overscrollOffset.snapTo(overscrollOffset.value + overscrollDelta.y * 0.1f)
@@ -105,17 +105,13 @@
override suspend fun consumePostFling(velocity: Velocity) {
// when the fling happens - we just gradually animate our overscroll to 0
- if (isEnabled) {
- overscrollOffset.animateTo(
- targetValue = 0f,
- initialVelocity = velocity.y,
- animationSpec = spring()
- )
- }
+ overscrollOffset.animateTo(
+ targetValue = 0f,
+ initialVelocity = velocity.y,
+ animationSpec = spring()
+ )
}
- override var isEnabled: Boolean by mutableStateOf(true)
-
override val isInProgress: Boolean
get() = overscrollOffset.isRunning
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
index b994fe6f..0db23d9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
@@ -110,7 +110,6 @@
)
rule.runOnIdle {
- // we passed isContentScrolls = 1, so initial draw must occur
assertThat(controller.drawCallsCount).isEqualTo(1)
}
@@ -144,7 +143,6 @@
)
rule.runOnIdle {
- // we passed isContentScrolls = 1, so initial draw must occur
assertThat(controller.drawCallsCount).isEqualTo(1)
}
@@ -158,7 +156,7 @@
// since we consume 1/10 of the delta in the pre scroll during overscroll, expect 9/10
assertThat(abs(acummulatedScroll - 1000f * 9 / 10)).isWithin(0.1f)
- assertThat(controller.preScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
+ assertThat(controller.lastPreScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
}
@@ -168,15 +166,24 @@
}
@Test
- fun overscrollEffect_scrollable_skipsDeltasIfDisabled() {
+ fun overscrollEffect_scrollable_skipsDeltasIfCannotScroll() {
var acummulatedScroll = 0f
val controller = TestOverscrollEffect(consumePreCycles = true)
+
+ var canScroll = true
+
val scrollableState = ScrollableState { delta ->
acummulatedScroll += delta
delta
}
+
val viewConfig = rule.setOverscrollContentAndReturnViewConfig(
- scrollableState = scrollableState,
+ scrollableState = object : ScrollableState by scrollableState {
+ override val canScrollForward: Boolean
+ get() = canScroll
+ override val canScrollBackward: Boolean
+ get() = canScroll
+ },
overscrollEffect = controller
)
@@ -186,15 +193,19 @@
up()
}
- val lastControlledConsumed = rule.runOnIdle {
+ rule.runOnIdle {
val slop = viewConfig.touchSlop
// since we consume 1/10 of the delta in the pre scroll during overscroll, expect 9/10
assertThat(abs(acummulatedScroll - 1000f * 9 / 10)).isWithin(0.1f)
- assertThat(controller.preScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
+ assertThat(controller.lastPreScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
- controller.isEnabled = false
- controller.preScrollDelta
+ controller.lastPreScrollDelta = Offset.Zero
+ }
+
+ // Inform scrollable that we cannot scroll anymore
+ rule.runOnIdle {
+ canScroll = false
}
rule.onNodeWithTag(boxTag).performTouchInput {
@@ -204,8 +215,8 @@
}
rule.runOnIdle {
- // still there because we are disabled
- assertThat(controller.preScrollDelta).isEqualTo(lastControlledConsumed)
+ // Scrollable should not have dispatched any new deltas
+ assertThat(controller.lastPreScrollDelta).isEqualTo(Offset.Zero)
}
}
@@ -231,7 +242,6 @@
)
rule.runOnIdle {
- // we passed isContentScrolls = 1, so initial draw must occur
assertThat(controller.drawCallsCount).isEqualTo(1)
}
@@ -355,7 +365,6 @@
}
rule.runOnIdle {
- controller.isEnabled = true
val offset = Offset(0f, 5f)
controller.consumePostScroll(
initialDragDelta = offset,
@@ -450,7 +459,6 @@
)
rule.runOnIdle {
- // we passed isContentScrolls = 1, so initial draw must occur
assertThat(controller.drawCallsCount).isEqualTo(1)
}
@@ -467,7 +475,7 @@
rule.runOnIdle {
with(controller) {
// presented on consume pre scroll
- assertSingleAxisValue(preScrollDelta.x, preScrollDelta.y)
+ assertSingleAxisValue(lastPreScrollDelta.x, lastPreScrollDelta.y)
// presented on consume post scroll
assertSingleAxisValue(lastOverscrollDelta.x, lastOverscrollDelta.y)
@@ -493,7 +501,6 @@
)
rule.runOnIdle {
- // we passed isContentScrolls = 1, so initial draw must occur
assertThat(controller.drawCallsCount).isEqualTo(1)
}
@@ -510,7 +517,7 @@
rule.runOnIdle {
with(controller) {
// presented on consume pre scroll
- assertSingleAxisValue(preScrollDelta.y, preScrollDelta.x)
+ assertSingleAxisValue(lastPreScrollDelta.y, lastPreScrollDelta.x)
// presented on consume post scroll
assertSingleAxisValue(lastOverscrollDelta.y, lastOverscrollDelta.x)
@@ -630,14 +637,13 @@
) : OverscrollEffect {
var drawCallsCount = 0
var isInProgressCallCount = 0
- var isContentScrolls = true
var lastVelocity = Velocity.Zero
var lastInitialDragDelta = Offset.Zero
var lastOverscrollDelta = Offset.Zero
var lastNestedScrollSource: NestedScrollSource? = null
- var preScrollDelta = Offset.Zero
+ var lastPreScrollDelta = Offset.Zero
var preScrollSource: NestedScrollSource? = null
var preFlingVelocity = Velocity.Zero
@@ -646,7 +652,7 @@
scrollDelta: Offset,
source: NestedScrollSource
): Offset {
- preScrollDelta = scrollDelta
+ lastPreScrollDelta = scrollDelta
preScrollSource = source
return if (consumePreCycles) scrollDelta / 10f else Offset.Zero
@@ -677,12 +683,6 @@
return animationRunning
}
- override var isEnabled: Boolean
- get() = isContentScrolls
- set(value) {
- isContentScrolls = value
- }
-
override val effectModifier: Modifier = Modifier.drawBehind { drawCallsCount += 1 }
}
@@ -703,7 +703,6 @@
)
rule.runOnIdle {
- // we passed isContentScrolls = 1, so initial draw must occur
assertThat(controller.drawCallsCount).isEqualTo(1)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 3ca102f..1496765 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -2565,8 +2565,6 @@
override suspend fun consumePostFling(velocity: Velocity) {}
- override var isEnabled: Boolean = false
-
override val isInProgress: Boolean
get() = false
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
index 179cc7d..2428fea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.list.BaseLazyListTestWithOrientation
@@ -30,6 +31,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -40,10 +43,14 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -229,6 +236,130 @@
}
}
+ @Test
+ fun performFling_shouldPropagateVelocityIfHitEdges() {
+ var stepSize = 0f
+ var latestAvailableVelocity = Velocity.Zero
+ lateinit var lazyListState: LazyListState
+ val inspectingNestedScrollConnection = object : NestedScrollConnection {
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ latestAvailableVelocity = available
+ return Velocity.Zero
+ }
+ }
+
+ // arrange
+ rule.setContent {
+ val density = LocalDensity.current
+ lazyListState = rememberLazyListState(180) // almost at the end
+ stepSize = with(density) { ItemSize.toPx() }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .nestedScroll(inspectingNestedScrollConnection)
+ ) {
+ MainLayout(state = lazyListState)
+ }
+ }
+
+ // act
+ onMainList().performTouchInput {
+ swipeMainAxisWithVelocity(
+ 1.5f * stepSize,
+ 30000f
+ )
+ }
+
+ // assert
+ rule.runOnIdle {
+ assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+ }
+
+ // arrange
+ rule.runOnIdle {
+ runBlocking {
+ lazyListState.scrollToItem(20) // almost at the start
+ }
+ }
+
+ latestAvailableVelocity = Velocity.Zero
+
+ // act
+ onMainList().performTouchInput {
+ swipeMainAxisWithVelocity(
+ -1.5f * stepSize,
+ 30000f
+ )
+ }
+
+ // assert
+ rule.runOnIdle {
+ assertNotEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+ }
+ }
+
+ @Test
+ fun performFling_shouldConsumeAllVelocityIfInTheMiddleOfTheList() {
+ var stepSize = 0f
+ var latestAvailableVelocity = Velocity.Zero
+ lateinit var lazyListState: LazyListState
+ val inspectingNestedScrollConnection = object : NestedScrollConnection {
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ latestAvailableVelocity = available
+ return Velocity.Zero
+ }
+ }
+
+ // arrange
+ rule.setContent {
+ val density = LocalDensity.current
+ lazyListState = rememberLazyListState(100) // middle of the list
+ stepSize = with(density) { ItemSize.toPx() }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .nestedScroll(inspectingNestedScrollConnection)
+ ) {
+ MainLayout(state = lazyListState)
+ }
+ }
+
+ // act
+ onMainList().performTouchInput {
+ swipeMainAxisWithVelocity(
+ 1.5f * stepSize,
+ 10000f // use a not so high velocity
+ )
+ }
+
+ // assert
+ rule.runOnIdle {
+ assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+ }
+
+ // arrange
+ rule.runOnIdle {
+ runBlocking {
+ lazyListState.scrollToItem(100) // return to the middle
+ }
+ }
+
+ latestAvailableVelocity = Velocity.Zero
+
+ // act
+ onMainList().performTouchInput {
+ swipeMainAxisWithVelocity(
+ -1.5f * stepSize,
+ 10000f // use a not so high velocity
+ )
+ }
+
+ // assert
+ rule.runOnIdle {
+ assertEquals(latestAvailableVelocity.toAbsoluteFloat(), 0f)
+ }
+ }
+
private fun onMainList() = rule.onNodeWithTag(TestTag)
@Composable
@@ -290,6 +421,10 @@
)
}
+ private fun Velocity.toAbsoluteFloat(): Float {
+ return (if (orientation == Orientation.Vertical) y else x).absoluteValue
+ }
+
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index 0c409ef..a09f72b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -67,6 +67,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlin.test.assertNotEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -111,7 +112,7 @@
}
@Test
- fun performFling_afterSnappingVelocity_shouldReturnNoVelocity() {
+ fun performFling_afterSnappingVelocity_everythingWasConsumed_shouldReturnNoVelocity() {
val testLayoutInfoProvider = TestLayoutInfoProvider()
var afterFlingVelocity = 0f
rule.setContent {
@@ -133,6 +134,29 @@
}
@Test
+ fun performFling_afterSnappingVelocity_didNotConsumeAllScroll_shouldReturnRemainingVelocity() {
+ val testLayoutInfoProvider = TestLayoutInfoProvider()
+ var afterFlingVelocity = 0f
+ rule.setContent {
+ // Consume only half
+ val scrollableState = rememberScrollableState(consumeScrollDelta = { it / 2f })
+ val testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+
+ LaunchedEffect(Unit) {
+ scrollableState.scroll {
+ afterFlingVelocity = with(testFlingBehavior) {
+ performFling(50000f)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertNotEquals(NoVelocity, afterFlingVelocity)
+ }
+ }
+
+ @Test
fun findClosestOffset_noFlingDirection_shouldReturnAbsoluteDistance() {
val testLayoutInfoProvider = TestLayoutInfoProvider()
val offset = findClosestOffset(0f, testLayoutInfoProvider, density)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
index 5d6408a..dd1e23b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -38,6 +38,7 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -250,6 +251,7 @@
assertThat(state.canScrollBackward).isFalse()
}
+ @Ignore("b/259608530")
@Test
fun canScrollBackward() = runBlocking {
withContext(Dispatchers.Main + AutoTestFrameClock()) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
index c34d2b2..3df360d 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
@@ -29,6 +29,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -73,6 +75,18 @@
)
}
+ internal fun axisSize(crossAxis: Int, mainAxis: Int): IntSize =
+ IntSize(
+ if (orientation == Orientation.Vertical) crossAxis else mainAxis,
+ if (orientation == Orientation.Vertical) mainAxis else crossAxis,
+ )
+
+ internal fun axisOffset(crossAxis: Int, mainAxis: Int): IntOffset =
+ IntOffset(
+ if (orientation == Orientation.Vertical) crossAxis else mainAxis,
+ if (orientation == Orientation.Vertical) mainAxis else crossAxis,
+ )
+
@Composable
internal fun LazyStaggeredGrid(
cells: StaggeredGridCells,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
index e55a4d1..c718b43 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
@@ -18,7 +18,6 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.ui.Modifier
@@ -310,4 +309,34 @@
.assertMainAxisSizeIsEqualTo(20.dp)
.assertCrossAxisSizeIsEqualTo(itemSizeDp * 2)
}
+
+ @Test
+ fun scrollsCorrectlyWithKeyAndLargeMainAxisContentPadding() {
+ state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+ rule.setContent {
+ LazyStaggeredGrid(
+ lanes = 2,
+ modifier = Modifier
+ .testTag(LazyStaggeredGrid)
+ .axisSize(itemSizeDp * 2, itemSizeDp * 5),
+ contentPadding = PaddingValues(
+ mainAxis = itemSizeDp * 2,
+ crossAxis = 0.dp
+ ),
+ state = state
+ ) {
+ items(1000, key = { it }) {
+ Spacer(Modifier.mainAxisSize(itemSizeDp).testTag("$it"))
+ }
+ }
+ }
+
+ repeat(10) {
+ state.scrollBy(itemSizeDp * 20)
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(400)
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index a9dc3a2..7eec8ab 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -95,6 +95,8 @@
"Visible items MUST BE sorted: ${state.layoutInfo.visibleItemsInfo}",
isSorted
)
+
+ assertThat(state.layoutInfo.orientation == orientation)
}
}
@@ -286,6 +288,53 @@
}
@Test
+ fun itemSizeInLayoutInfo() {
+ rule.setContent {
+ state = rememberLazyStaggeredGridState()
+ LazyStaggeredGrid(
+ lanes = 3,
+ state = state,
+ modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+ ) {
+ items(6) {
+ Spacer(
+ Modifier
+ .axisSize(
+ crossAxis = itemSizeDp,
+ mainAxis = itemSizeDp * (it + 1)
+ )
+ .testTag("$it")
+ .debugBorder()
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSizeDp * 3)
+
+ val items = state.layoutInfo.visibleItemsInfo
+
+ assertThat(items.size).isEqualTo(3)
+ with(items[0]) {
+ assertThat(index).isEqualTo(3)
+ assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 4))
+ assertThat(offset).isEqualTo(axisOffset(0, -itemSizePx * 2))
+ }
+
+ with(items[1]) {
+ assertThat(index).isEqualTo(4)
+ assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 5))
+ assertThat(offset).isEqualTo(axisOffset(itemSizePx, -itemSizePx))
+ }
+
+ with(items[2]) {
+ assertThat(index).isEqualTo(5)
+ assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 6))
+ assertThat(offset).isEqualTo(axisOffset(itemSizePx * 2, 0))
+ }
+ }
+
+ @Test
fun itemCanEmitZeroNodes() {
rule.setContent {
state = rememberLazyStaggeredGridState()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
index 32047cf..b651632 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
@@ -252,21 +252,6 @@
private var containerSize = Size.Zero
- private val isEnabledState = mutableStateOf(false)
- override var isEnabled: Boolean = false
- get() = isEnabledState.value
- set(value) {
- // Use field instead of isEnabledState to avoid a state read
- val enabledChanged = field != value
- isEnabledState.value = value
- field = value
-
- if (enabledChanged) {
- scrollCycleInProgress = false
- animateToRelease()
- }
- }
-
override val isInProgress: Boolean
get() {
return allEffects.fastAny { it.distanceCompat != 0f }
@@ -523,8 +508,6 @@
override suspend fun consumePostFling(velocity: Velocity) {}
- override var isEnabled: Boolean = false
-
override val isInProgress: Boolean
get() = false
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
index bea29df..b56d000 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
@@ -96,12 +96,6 @@
suspend fun consumePostFling(velocity: Velocity)
/**
- * Whether the overscroll effect is enabled or not. If it is not enabled, [scrollable] won't
- * send the events to this effect.
- */
- var isEnabled: Boolean
-
- /**
* Whether over scroll within this controller is currently on progress or not, e.g. if the
* overscroll effect is playing animation or shown/interactable in any other way.
*
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 9c1374a..76621b48 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -302,7 +302,7 @@
overscrollEffect = overscrollEffect
)
val layout =
- ScrollingLayoutModifier(state, reverseScrolling, isVertical, overscrollEffect)
+ ScrollingLayoutModifier(state, reverseScrolling, isVertical)
semantics
.clipScrollableContainer(orientation)
.overscroll(overscrollEffect)
@@ -319,12 +319,10 @@
}
)
-@OptIn(ExperimentalFoundationApi::class)
private data class ScrollingLayoutModifier(
val scrollerState: ScrollState,
val isReversed: Boolean,
- val isVertical: Boolean,
- val overscrollEffect: OverscrollEffect
+ val isVertical: Boolean
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
@@ -345,7 +343,6 @@
val scrollHeight = placeable.height - height
val scrollWidth = placeable.width - width
val side = if (isVertical) scrollHeight else scrollWidth
- overscrollEffect.isEnabled = side != 0
// The max value must be updated before returning from the measure block so that any other
// chained RemeasurementModifiers that try to perform scrolling based on the new
// measurements inside onRemeasured are able to scroll to the new max based on the newly-
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 7fa7897..3876703 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -390,11 +390,14 @@
return overscrollPreConsumed + preConsumedByParent + axisConsumed + parentConsumed
}
+ private val shouldDispatchOverscroll
+ get() = scrollableState.canScrollForward || scrollableState.canScrollBackward
+
fun overscrollPreConsumeDelta(
scrollDelta: Offset,
source: NestedScrollSource
): Offset {
- return if (overscrollEffect != null && overscrollEffect.isEnabled) {
+ return if (overscrollEffect != null && shouldDispatchOverscroll) {
overscrollEffect.consumePreScroll(scrollDelta, source)
} else {
Offset.Zero
@@ -406,7 +409,7 @@
availableForOverscroll: Offset,
source: NestedScrollSource
) {
- if (overscrollEffect != null && overscrollEffect.isEnabled) {
+ if (overscrollEffect != null && shouldDispatchOverscroll) {
overscrollEffect.consumePostScroll(
consumedByChain,
availableForOverscroll,
@@ -430,7 +433,7 @@
val availableVelocity = initialVelocity.singleAxisVelocity()
val preOverscrollConsumed =
- if (overscrollEffect != null && overscrollEffect.isEnabled) {
+ if (overscrollEffect != null && shouldDispatchOverscroll) {
overscrollEffect.consumePreFling(availableVelocity)
} else {
Velocity.Zero
@@ -446,7 +449,7 @@
velocityLeft
)
val totalLeft = velocityLeft - consumedPost
- if (overscrollEffect != null && overscrollEffect.isEnabled) {
+ if (overscrollEffect != null && shouldDispatchOverscroll) {
overscrollEffect.consumePostFling(totalLeft)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index b1e05f7..59b76f4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -95,24 +95,32 @@
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
// If snapping from scroll (short snap) or fling (long snap)
- withContext(motionScaleDuration) {
+ val (remainingOffset, remainingState) = withContext(motionScaleDuration) {
if (abs(initialVelocity) <= abs(velocityThreshold)) {
shortSnap(initialVelocity)
} else {
longSnap(initialVelocity)
}
}
- return NoVelocity
+ debugLog { "Post Settling Offset=$remainingOffset" }
+ // No remaining offset means we've used everything, no need to propagate velocity. Otherwise
+ // we couldn't use everything (probably because we have hit the min/max bounds of the
+ // containing layout) we should propagate the offset.
+ return if (remainingOffset == 0f) NoVelocity else remainingState.velocity
}
- private suspend fun ScrollScope.shortSnap(velocity: Float) {
+ private suspend fun ScrollScope.shortSnap(
+ velocity: Float
+ ): AnimationResult<Float, AnimationVector1D> {
debugLog { "Short Snapping" }
val closestOffset = findClosestOffset(0f, snapLayoutInfoProvider, density)
val animationState = AnimationState(NoDistance, velocity)
- animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
+ return animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
}
- private suspend fun ScrollScope.longSnap(initialVelocity: Float) {
+ private suspend fun ScrollScope.longSnap(
+ initialVelocity: Float
+ ): AnimationResult<Float, AnimationVector1D> {
debugLog { "Long Snapping" }
val initialOffset =
with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
@@ -123,7 +131,7 @@
debugLog { "Settling Final Bound=$remainingOffset" }
- animateSnap(
+ return animateSnap(
remainingOffset,
remainingOffset,
animationState.copy(value = 0f),
@@ -134,7 +142,7 @@
private suspend fun ScrollScope.runApproach(
initialTargetOffset: Float,
initialVelocity: Float
- ): ApproachStepResult {
+ ): AnimationResult<Float, AnimationVector1D> {
val animation =
if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
@@ -237,22 +245,28 @@
animation: ApproachAnimation<Float, AnimationVector1D>,
snapLayoutInfoProvider: SnapLayoutInfoProvider,
density: Density
-): ApproachStepResult {
+): AnimationResult<Float, AnimationVector1D> {
- val currentAnimationState =
- animation.approachAnimation(this, initialTargetOffset, initialVelocity)
+ val (_, currentAnimationState) = animation.approachAnimation(
+ this,
+ initialTargetOffset,
+ initialVelocity
+ )
val remainingOffset =
findClosestOffset(currentAnimationState.velocity, snapLayoutInfoProvider, density)
// will snap the remainder
- return ApproachStepResult(remainingOffset, currentAnimationState)
+ return AnimationResult(remainingOffset, currentAnimationState)
}
-private data class ApproachStepResult(
- val remainingOffset: Float,
- val currentAnimationState: AnimationState<Float, AnimationVector1D>
-)
+private class AnimationResult<T, V : AnimationVector>(
+ val remainingOffset: T,
+ val currentAnimationState: AnimationState<T, V>
+) {
+ operator fun component1(): T = remainingOffset
+ operator fun component2(): AnimationState<T, V> = currentAnimationState
+}
/**
* Finds the closest offset to snap to given the Fling Direction.
@@ -309,7 +323,7 @@
targetOffset: Float,
animationState: AnimationState<Float, AnimationVector1D>,
decayAnimationSpec: DecayAnimationSpec<Float>
-): AnimationState<Float, AnimationVector1D> {
+): AnimationResult<Float, AnimationVector1D> {
var previousValue = 0f
fun AnimationScope<Float, AnimationVector1D>.consumeDelta(delta: Float) {
@@ -332,7 +346,10 @@
previousValue = value
}
}
- return animationState
+ return AnimationResult(
+ targetOffset - previousValue,
+ animationState
+ )
}
/**
@@ -344,7 +361,7 @@
cancelOffset: Float,
animationState: AnimationState<Float, AnimationVector1D>,
snapAnimationSpec: AnimationSpec<Float>
-): AnimationState<Float, AnimationVector1D> {
+): AnimationResult<Float, AnimationVector1D> {
var consumedUpToNow = 0f
val initialVelocity = animationState.velocity
animationState.animateTo(
@@ -359,12 +376,15 @@
if (abs(delta - consumed) > 0.5f || realValue != value) {
cancelAnimation()
}
- consumedUpToNow += delta
+ consumedUpToNow += consumed
}
// Always course correct velocity so they don't become too large.
val finalVelocity = animationState.velocity.coerceToTarget(initialVelocity)
- return animationState.copy(velocity = finalVelocity)
+ return AnimationResult(
+ targetOffset - consumedUpToNow,
+ animationState.copy(velocity = finalVelocity)
+ )
}
private fun Float.coerceToTarget(target: Float): Float {
@@ -380,7 +400,7 @@
scope: ScrollScope,
offset: T,
velocity: T
- ): AnimationState<T, V>
+ ): AnimationResult<T, V>
}
@OptIn(ExperimentalFoundationApi::class)
@@ -393,7 +413,7 @@
scope: ScrollScope,
offset: Float,
velocity: Float
- ): AnimationState<Float, AnimationVector1D> {
+ ): AnimationResult<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
val targetOffset =
(abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
@@ -417,7 +437,7 @@
scope: ScrollScope,
offset: Float,
velocity: Float
- ): AnimationState<Float, AnimationVector1D> {
+ ): AnimationResult<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
return with(scope) {
animateDecay(offset, animationState, decayAnimationSpec)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index a28a5e4..ec2d8b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.checkScrollableContainerConstraints
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.FlingBehavior
@@ -91,7 +90,6 @@
itemProvider,
state,
beyondBoundsInfo,
- overscrollEffect,
contentPadding,
reverseLayout,
isVertical,
@@ -160,8 +158,6 @@
state: LazyListState,
/** Keeps track of the number of items we measure and place that are beyond visible bounds. */
beyondBoundsInfo: LazyListBeyondBoundsInfo,
- /** The overscroll controller. */
- overscrollEffect: OverscrollEffect,
/** The inner padding to be added for the whole content(nor for each individual item) */
contentPadding: PaddingValues,
/** reverse the direction of scrolling and layout */
@@ -183,7 +179,6 @@
) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
state,
beyondBoundsInfo,
- overscrollEffect,
contentPadding,
reverseLayout,
isVertical,
@@ -332,19 +327,6 @@
}
).also {
state.applyMeasureResult(it)
- refreshOverscrollInfo(overscrollEffect, it)
}
}
}
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun refreshOverscrollInfo(
- overscrollEffect: OverscrollEffect,
- result: LazyListMeasureResult
-) {
- val canScrollForward = result.canScrollForward
- val canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
- result.firstVisibleItemScrollOffset != 0
-
- overscrollEffect.isEnabled = canScrollForward || canScrollBackward
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 79c1683..83ec3f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.checkScrollableContainerConstraints
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.FlingBehavior
@@ -89,7 +88,6 @@
val measurePolicy = rememberLazyGridMeasurePolicy(
itemProvider,
state,
- overscrollEffect,
slotSizesSums,
contentPadding,
reverseLayout,
@@ -154,8 +152,6 @@
itemProvider: LazyGridItemProvider,
/** The state of the list. */
state: LazyGridState,
- /** The overscroll controller. */
- overscrollEffect: OverscrollEffect,
/** Prefix sums of cross axis sizes of slots of the grid. */
slotSizesSums: Density.(Constraints) -> List<Int>,
/** The inner padding to be added for the whole content(nor for each individual item) */
@@ -172,7 +168,6 @@
placementAnimator: LazyGridItemPlacementAnimator
) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
state,
- overscrollEffect,
slotSizesSums,
contentPadding,
reverseLayout,
@@ -360,19 +355,6 @@
}
).also {
state.applyMeasureResult(it)
- refreshOverscrollInfo(overscrollEffect, it)
}
}
}
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun refreshOverscrollInfo(
- overscrollEffect: OverscrollEffect,
- result: LazyGridMeasureResult
-) {
- val canScrollForward = result.canScrollForward
- val canScrollBackward = (result.firstVisibleLine?.items?.firstOrNull() ?: 0) != 0 ||
- result.firstVisibleLineScrollOffset != 0
-
- overscrollEffect.isEnabled = canScrollForward || canScrollBackward
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
index 7c11f76..2630e7d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
@@ -72,8 +72,7 @@
orientation,
verticalArrangement,
horizontalArrangement,
- slotSizesSums,
- overscrollEffect
+ slotSizesSums
)
val semanticState = rememberLazyStaggeredGridSemanticState(state, itemProvider, reverseLayout)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index e0f601d..138d229 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -177,13 +177,14 @@
measureResult = layout(constraints.minWidth, constraints.minHeight) {},
canScrollForward = false,
canScrollBackward = false,
+ isVertical = isVertical,
visibleItemsInfo = emptyList(),
totalItemsCount = itemCount,
viewportSize = IntSize(constraints.minWidth, constraints.minHeight),
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
beforeContentPadding = beforeContentPadding,
- afterContentPadding = afterContentPadding
+ afterContentPadding = afterContentPadding,
)
}
@@ -560,6 +561,7 @@
},
canScrollForward = canScrollForward,
canScrollBackward = canScrollBackward,
+ isVertical = isVertical,
visibleItemsInfo = positionedItems.asMutableList(),
totalItemsCount = itemCount,
viewportSize = IntSize(layoutWidth, layoutHeight),
@@ -713,7 +715,11 @@
lane = lane,
index = index,
key = key,
- size = IntSize(sizeWithSpacings, crossAxisSize),
+ size = if (isVertical) {
+ IntSize(crossAxisSize, sizeWithSpacings)
+ } else {
+ IntSize(sizeWithSpacings, crossAxisSize)
+ },
placeables = placeables,
contentOffset = contentOffset,
isVertical = isVertical
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
index 4b9097f..a645632 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.checkScrollableContainerConstraints
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
@@ -46,8 +45,7 @@
orientation: Orientation,
verticalArrangement: Arrangement.Vertical,
horizontalArrangement: Arrangement.Horizontal,
- slotSizesSums: Density.(Constraints) -> IntArray,
- overscrollEffect: OverscrollEffect
+ slotSizesSums: Density.(Constraints) -> IntArray
): LazyLayoutMeasureScope.(Constraints) -> LazyStaggeredGridMeasureResult = remember(
state,
itemProvider,
@@ -56,8 +54,7 @@
orientation,
verticalArrangement,
horizontalArrangement,
- slotSizesSums,
- overscrollEffect,
+ slotSizesSums
) {
{ constraints ->
checkScrollableContainerConstraints(
@@ -125,7 +122,6 @@
afterContentPadding = afterContentPadding,
).also {
state.applyMeasureResult(it)
- overscrollEffect.isEnabled = it.canScrollForward || it.canScrollBackward
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
index e63287a..352a762 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -63,6 +64,11 @@
@ExperimentalFoundationApi
sealed interface LazyStaggeredGridLayoutInfo {
/**
+ * Orientation of the staggered grid.
+ */
+ val orientation: Orientation
+
+ /**
* The list of [LazyStaggeredGridItemInfo] per each visible item ordered by index.
*/
val visibleItemsInfo: List<LazyStaggeredGridItemInfo>
@@ -129,6 +135,7 @@
val measureResult: MeasureResult,
val canScrollForward: Boolean,
val canScrollBackward: Boolean,
+ val isVertical: Boolean,
override val totalItemsCount: Int,
override val visibleItemsInfo: List<LazyStaggeredGridItemInfo>,
override val viewportSize: IntSize,
@@ -136,7 +143,10 @@
override val viewportEndOffset: Int,
override val beforeContentPadding: Int,
override val afterContentPadding: Int
-) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult
+) : LazyStaggeredGridLayoutInfo, MeasureResult by measureResult {
+ override val orientation: Orientation =
+ if (isVertical) Orientation.Vertical else Orientation.Horizontal
+}
@OptIn(ExperimentalFoundationApi::class)
internal object EmptyLazyStaggeredGridLayoutInfo : LazyStaggeredGridLayoutInfo {
@@ -147,4 +157,5 @@
override val viewportEndOffset: Int = 0
override val beforeContentPadding: Int = 0
override val afterContentPadding: Int = 0
+ override val orientation: Orientation = Orientation.Vertical
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
index b1a3529..5d5fe33 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
@@ -23,6 +23,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.util.fastFirstOrNull
@ExperimentalFoundationApi
internal class LazyStaggeredGridScrollPosition(
@@ -42,7 +43,13 @@
* Updates the current scroll position based on the results of the last measurement.
*/
fun updateFromMeasureResult(measureResult: LazyStaggeredGridMeasureResult) {
- lastKnownFirstItemKey = measureResult.visibleItemsInfo.firstOrNull()?.key
+ val firstVisibleIndex = measureResult.firstVisibleItemIndices
+ .minBy { if (it == -1) Int.MAX_VALUE else it }
+ .let { if (it == Int.MAX_VALUE) 0 else it }
+
+ lastKnownFirstItemKey = measureResult.visibleItemsInfo
+ .fastFirstOrNull { it.index == firstVisibleIndex }
+ ?.key
// we ignore the index and offset from measureResult until we get at least one
// measurement with real items. otherwise the initial index and scroll passed to the
// state would be lost and overridden with zeros.
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
index 3c53ec5..75bbcfc 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DesktopOverscroll.kt
@@ -48,7 +48,6 @@
override suspend fun consumePostFling(velocity: Velocity) {}
- override var isEnabled = false
override val isInProgress = false
override val effectModifier = Modifier
}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index e883852..8081212 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -16,24 +16,20 @@
package androidx.compose.integration.demos
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.integration.demos.common.Demo
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
@@ -44,7 +40,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
@@ -58,9 +53,8 @@
val filteredDemos = launchableDemos
.filter { it.title.contains(filterText, ignoreCase = true) }
.sortedBy { it.title }
- // TODO: migrate to LazyColumn after b/175671850
- Column(Modifier.verticalScroll(rememberScrollState())) {
- filteredDemos.forEach { demo ->
+ LazyColumn {
+ items(filteredDemos) { demo ->
FilteredDemoListItem(
demo,
filterText = filterText,
@@ -99,23 +93,20 @@
}
/**
- * [BasicTextField] that edits the current [filterText], providing [onFilter] when edited.
+ * [TextField] that edits the current [filterText], providing [onFilter] when edited.
*/
@Composable
-@OptIn(ExperimentalFoundationApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
private fun FilterField(
filterText: String,
onFilter: (String) -> Unit,
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
- // TODO: replace with Material text field when available
- BasicTextField(
+ TextField(
modifier = modifier.focusRequester(focusRequester),
value = filterText,
- onValueChange = onFilter,
- textStyle = LocalTextStyle.current,
- cursorBrush = SolidColor(LocalContentColor.current)
+ onValueChange = onFilter
)
DisposableEffect(focusRequester) {
focusRequester.requestFocus()
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 1b36239..e77373f 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -112,11 +112,3 @@
android {
namespace "androidx.compose.material.ripple"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index ec09563..c553c4b 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -383,8 +383,8 @@
property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
- property @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.material.ExperimentalMaterialApi public final Float? offset;
- property @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
+ property @androidx.compose.material.ExperimentalMaterialApi public final Float? offset;
+ property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
field public static final androidx.compose.material.DrawerState.Companion Companion;
}
@@ -416,7 +416,7 @@
@kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterialApi {
}
- @androidx.compose.material.ExperimentalMaterialApi public interface ExposedDropdownMenuBoxScope {
+ @androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface ExposedDropdownMenuBoxScope {
method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method public androidx.compose.ui.Modifier exposedDropdownSize(androidx.compose.ui.Modifier, optional boolean matchTextFieldWidth);
}
@@ -844,7 +844,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError);
}
- @androidx.compose.material.ExperimentalMaterialApi public interface TextFieldColorsWithIcons extends androidx.compose.material.TextFieldColors {
+ @androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface TextFieldColorsWithIcons extends androidx.compose.material.TextFieldColors {
method @androidx.compose.runtime.Composable public default androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
method @androidx.compose.runtime.Composable public default androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
new file mode 100644
index 0000000..1cfa222
--- /dev/null
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.pullrefresh
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.testutils.assertPixelColor
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterialApi::class)
+class PullRefreshIndicatorTransformTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ // Convert from floats to DP to avoid rounding issues later
+
+ private val IndicatorSize get() = with(rule.density) { 200f.toDp() }
+ // Make the box large enough so that when the indicator is offset when not shown, it is still
+ // offset to within the bounds of the containing box
+ private val ContainingBoxSize = with(rule.density) { 800f.toDp() }
+ private val BoxTag = "Box"
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indicatorClippedWhenNotDisplayed() {
+ rule.setContent {
+ val state = rememberPullRefreshState(refreshing = false, {})
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .wrapContentSize(Alignment.Center)
+ // Set a larger size so that when offset the indicator will still appear
+ .size(ContainingBoxSize)
+ .testTag(BoxTag),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(Modifier
+ .pullRefreshIndicatorTransform(state)
+ .size(IndicatorSize)
+ .background(Color.Black)
+ )
+ }
+ }
+ // The indicator should be fully clipped and invisible
+ rule.onNodeWithTag(BoxTag).captureToImage().assertPixels { Color.White }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indicatorPartiallyClippedWhenPartiallyDisplayed() {
+ lateinit var state: PullRefreshState
+ rule.setContent {
+ // Pull down by 100 pixels (the actual position delta is half of this because the state
+ // applies a multiplier)
+ state = rememberPullRefreshState(refreshing = false, onRefresh = {}).apply {
+ onPull(100f)
+ }
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .wrapContentSize(Alignment.Center)
+ // Set a larger size so that when offset the indicator will still appear
+ .size(ContainingBoxSize)
+ .testTag(BoxTag),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(Modifier
+ .pullRefreshIndicatorTransform(state)
+ .size(IndicatorSize)
+ .background(Color.Black)
+ )
+ }
+ }
+ // The indicator should be partially clipped
+ rule.onNodeWithTag(BoxTag).captureToImage().run {
+ val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorXRange = with(rule.density) {
+ indicatorStart until (indicatorStart + IndicatorSize.toPx()).toInt()
+ }
+
+ val indicatorTop = with(rule.density) { height / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorYRange = indicatorTop until indicatorTop + state.position.toInt()
+
+ val pixel = toPixelMap()
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val expectedColor = if (x in indicatorXRange && y in indicatorYRange) {
+ Color.Black
+ } else {
+ Color.White
+ }
+ pixel.assertPixelColor(expectedColor, x, y)
+ }
+ }
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indicatorNotClippedWhenFullyDisplayed() {
+ lateinit var state: PullRefreshState
+ rule.setContent {
+ // Set refreshing and set the refreshing offset to match the indicator size -
+ // this means that the indicator will start to show at an offset of 0 from its normal
+ // layout position, since by default it is negatively offset by its height
+ state = rememberPullRefreshState(
+ refreshing = true,
+ onRefresh = {},
+ refreshingOffset = IndicatorSize
+ )
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .wrapContentSize(Alignment.Center)
+ // Set a larger size so that when offset the indicator will still appear
+ .size(ContainingBoxSize)
+ .testTag(BoxTag),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(Modifier
+ .pullRefreshIndicatorTransform(state)
+ .size(IndicatorSize)
+ .background(Color.Black)
+ )
+ }
+ }
+ // The indicator should be fully visible
+ rule.onNodeWithTag(BoxTag).captureToImage().run {
+ val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorXRange = with(rule.density) {
+ indicatorStart until (indicatorStart + IndicatorSize.toPx()).toInt()
+ }
+
+ val indicatorTop = with(rule.density) { height / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorYRange = with(rule.density) {
+ indicatorTop until (indicatorTop + IndicatorSize.toPx()).toInt()
+ }
+
+ val pixel = toPixelMap()
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val expectedColor = if (x in indicatorXRange && y in indicatorYRange) {
+ Color.Black
+ } else {
+ Color.White
+ }
+ pixel.assertPixelColor(expectedColor, x, y)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
index 63c1027..fe4190a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
@@ -79,13 +79,14 @@
@Test
fun pullLessThanOrEqualToThreshold_doesNot_triggerRefresh() {
+ lateinit var state: PullRefreshState
var refreshCount = 0
var touchSlop = 0f
val threshold = 400f
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- val state = rememberPullRefreshState(
+ state = rememberPullRefreshState(
refreshing = false,
onRefresh = { refreshCount++ },
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
@@ -110,7 +111,12 @@
// Equal to threshold
pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop) }
- rule.runOnIdle { assertThat(refreshCount).isEqualTo(0) }
+ rule.runOnIdle {
+ assertThat(refreshCount).isEqualTo(0)
+ // Since onRefresh was not called, we should reset the position back to 0
+ assertThat(state.progress).isEqualTo(0f)
+ assertThat(state.position).isEqualTo(0f)
+ }
}
@Test
@@ -309,6 +315,43 @@
}
}
+ @Test
+ fun pullBeyondThreshold_refreshingNotChangedToTrue_animatePositionBackToZero() {
+ lateinit var state: PullRefreshState
+ var refreshCount = 0
+ var touchSlop = 0f
+ val threshold = 400f
+
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ state = rememberPullRefreshState(
+ refreshing = false,
+ onRefresh = { refreshCount++ },
+ refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
+ )
+
+ Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ LazyColumn {
+ items(100) {
+ Text("item $it")
+ }
+ }
+ }
+ }
+
+ // Account for PullModifier - pull down twice the threshold value.
+ pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop + 1f) }
+
+ rule.runOnIdle {
+ // onRefresh should be called
+ assertThat(refreshCount).isEqualTo(1)
+ // Since onRefresh did not change refreshing, we should have reset the position back to
+ // 0
+ assertThat(state.progress).isEqualTo(0f)
+ assertThat(state.position).isEqualTo(0f)
+ }
+ }
+
/**
* Taken from the private function of the same name in [PullRefreshState].
*/
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index cfaa288..4a46e56 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -190,6 +190,7 @@
}
}
+@JvmDefaultWithCompatibility
/**
* Scope for [ExposedDropdownMenuBox].
*/
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index 9ee4ffb..21d797a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -63,7 +63,7 @@
*
* @param state The associated [SwipeableV2State].
* @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
* @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
* swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
index 975361d..dc0a4da 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldDefaults.kt
@@ -141,6 +141,7 @@
fun cursorColor(isError: Boolean): State<Color>
}
+@JvmDefaultWithCompatibility
/**
* Temporary experimental interface, to expose interactionSource to
* leadingIconColor and trailingIconColor.
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
index 7ca9609..ad97176 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
@@ -18,15 +18,12 @@
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.platform.inspectable
/**
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
@@ -42,17 +39,30 @@
fun Modifier.pullRefreshIndicatorTransform(
state: PullRefreshState,
scale: Boolean = false,
-) = composed(inspectorInfo = debugInspectorInfo {
+) = inspectable(inspectorInfo = debugInspectorInfo {
name = "pullRefreshIndicatorTransform"
properties["state"] = state
properties["scale"] = scale
}) {
- var height by remember { mutableStateOf(0) }
-
Modifier
- .onSizeChanged { height = it.height }
+ // Essentially we only want to clip the at the top, so the indicator will not appear when
+ // the position is 0. It is preferable to clip the indicator as opposed to the layout that
+ // contains the indicator, as this would also end up clipping shadows drawn by items in a
+ // list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
+ // for the other dimensions to allow for more room for elevation / arbitrary indicators - we
+ // only ever really want to clip at the top edge.
+ .drawWithContent {
+ clipRect(
+ top = 0f,
+ left = -Float.MAX_VALUE,
+ right = Float.MAX_VALUE,
+ bottom = Float.MAX_VALUE
+ ) {
+ this@drawWithContent.drawContent()
+ }
+ }
.graphicsLayer {
- translationY = state.position - height
+ translationY = state.position - size.height
if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
index e673400..b425a38 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
@@ -17,6 +17,7 @@
package androidx.compose.material.pullrefresh
import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatorMutex
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@@ -136,9 +137,8 @@
if (!this._refreshing) {
if (adjustedDistancePulled > threshold) {
onRefreshState.value()
- } else {
- animateIndicatorTo(0f)
}
+ animateIndicatorTo(0f)
}
distancePulled = 0f
}
@@ -151,9 +151,16 @@
}
}
+ // Make sure to cancel any existing animations when we launch a new one. We use this instead of
+ // Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
+ // overhead of running through the animation pipeline instead of directly mutating the state.
+ private val mutatorMutex = MutatorMutex()
+
private fun animateIndicatorTo(offset: Float) = animationScope.launch {
- animate(initialValue = _position, targetValue = offset) { value, _ ->
- _position = value
+ mutatorMutex.mutate {
+ animate(initialValue = _position, targetValue = offset) { value, _ ->
+ _position = value
+ }
}
}
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index 5f1fc46..ad2abb0 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -113,11 +113,3 @@
android {
namespace "androidx.compose.material3.windowsizeclass"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 8230350..d92a1b2 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -326,8 +326,8 @@
property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
- property @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.runtime.State<java.lang.Float> offset;
- property @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.material3.DrawerValue targetValue;
+ property @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.runtime.State<java.lang.Float> offset;
+ property @androidx.compose.material3.ExperimentalMaterial3Api public final androidx.compose.material3.DrawerValue targetValue;
field public static final androidx.compose.material3.DrawerState.Companion Companion;
}
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 7a79167..fb360a4 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -160,11 +160,3 @@
project.rootDir.absolutePath + "/../../golden/compose/material3/material3"
namespace "androidx.compose.material3"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/build.gradle b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
index 189003f..28da77e 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
@@ -46,11 +46,3 @@
android {
namespace "androidx.compose.material3.catalog.library"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index 4849ec9..021de9a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -35,8 +35,15 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
@@ -46,19 +53,19 @@
import kotlinx.coroutines.launch
/**
- * Enable swipe gestures between a set of predefined states.
+ * Enable swipe gestures between a set of predefined values.
*
* When a swipe is detected, the offset of the [SwipeableV2State] will be updated with the swipe
* delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
* When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableV2State] will also be updated to the state corresponding to
+ * reached, the value of the [SwipeableV2State] will also be updated to the value corresponding to
* the new anchor.
*
* Swiping is constrained between the minimum and maximum anchors.
*
* @param state The associated [SwipeableV2State].
* @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
* @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
* swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to
@@ -86,36 +93,46 @@
* the state with them.
*
* @param state The associated [SwipeableV2State]
- * @param possibleStates All possible states the [SwipeableV2State] could be in.
+ * @param possibleValues All possible values the [SwipeableV2State] could be in.
* @param anchorsChanged A callback to be invoked when the anchors have changed, `null` by default.
* Components with custom reconciliation logic should implement this callback, i.e. to re-target an
* in-progress animation.
* @param calculateAnchor This method will be invoked to calculate the position of all
- * [possibleStates], given this node's layout size. Return the anchor's offset from the initial
- * anchor, or `null` to indicate that a state does not exist.
+ * [possibleValues], given this node's layout size. Return the anchor's offset from the initial
+ * anchor, or `null` to indicate that a value does not have an anchor.
*/
@ExperimentalMaterial3Api
internal fun <T> Modifier.swipeAnchors(
state: SwipeableV2State<T>,
- possibleStates: Set<T>,
+ possibleValues: Set<T>,
anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
- calculateAnchor: (state: T, layoutSize: IntSize) -> Float?,
-) = onSizeChanged { layoutSize ->
- val previousAnchors = state.anchors
- val newAnchors = mutableMapOf<T, Float>()
- possibleStates.forEach {
- val anchorValue = calculateAnchor(it, layoutSize)
- if (anchorValue != null) {
- newAnchors[it] = anchorValue
+ calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
+) = this.then(SwipeAnchorsModifier(
+ onDensityChanged = { state.density = it },
+ onSizeChanged = { layoutSize ->
+ val previousAnchors = state.anchors
+ val newAnchors = mutableMapOf<T, Float>()
+ possibleValues.forEach {
+ val anchorValue = calculateAnchor(it, layoutSize)
+ if (anchorValue != null) {
+ newAnchors[it] = anchorValue
+ }
}
+ if (previousAnchors != newAnchors) {
+ state.updateAnchors(newAnchors)
+ if (previousAnchors.isNotEmpty()) {
+ anchorsChanged?.invoke(previousAnchors, newAnchors)
+ }
+ }
+ },
+ inspectorInfo = debugInspectorInfo {
+ name = "swipeAnchors"
+ properties["state"] = state
+ properties["possibleValues"] = possibleValues
+ properties["anchorsChanged"] = anchorsChanged
+ properties["calculateAnchor"] = calculateAnchor
}
- if (previousAnchors == newAnchors) return@onSizeChanged
- state.updateAnchors(newAnchors)
-
- if (previousAnchors.isNotEmpty()) {
- anchorsChanged?.invoke(previousAnchors, newAnchors)
- }
-}
+))
/**
* State of the [swipeableV2] modifier.
@@ -124,10 +141,9 @@
* to change the state either immediately or by starting an animation. To create and remember a
* [SwipeableV2State] use [rememberSwipeableV2State].
*
- * @param initialState The initial value of the state.
- * @param density The density used to convert thresholds from px to dp.
+ * @param initialValue The initial value of the state.
* @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param positionalThreshold The positional threshold to be used when calculating the target state
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
* the start of a transition. It will be, depending on the direction of the interaction, added or
@@ -140,31 +156,30 @@
@Stable
@ExperimentalMaterial3Api
internal class SwipeableV2State<T>(
- initialState: T,
- internal val density: Density,
+ initialValue: T,
internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true },
+ internal val confirmValueChange: (newValue: T) -> Boolean = { true },
internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
SwipeableV2Defaults.PositionalThreshold,
internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
) {
/**
- * The current state of the [SwipeableV2State].
+ * The current value of the [SwipeableV2State].
*/
- var currentState: T by mutableStateOf(initialState)
+ var currentValue: T by mutableStateOf(initialValue)
private set
/**
- * The target state. This is the closest state to the current offset (taking into account
+ * The target value. This is the closest value to the current offset (taking into account
* positional thresholds). If no interactions like animations or drags are in progress, this
- * will be the current state.
+ * will be the current value.
*/
- val targetState: T by derivedStateOf {
+ val targetValue: T by derivedStateOf {
val currentOffset = offset
if (currentOffset != null) {
- computeTarget(currentOffset, currentState, velocity = 0f)
- } else currentState
+ computeTarget(currentOffset, currentValue, velocity = 0f)
+ } else currentValue
}
/**
@@ -180,6 +195,7 @@
*
* To guarantee stricter semantics, consider using [requireOffset].
*/
+ @get:Suppress("AutoBoxing")
val offset: Float? by derivedStateOf {
dragPosition?.coerceIn(minBound, maxBound)
}
@@ -201,12 +217,13 @@
private set
/**
- * The fraction of the progress going from currentState to targetState, within [0f..1f] bounds.
+ * The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
+ * bounds.
*/
/*@FloatRange(from = 0f, to = 1f)*/
val progress: Float by derivedStateOf {
- val a = anchors[currentState] ?: 0f
- val b = anchors[targetState] ?: 0f
+ val a = anchors[currentValue] ?: 0f
+ val b = anchors[targetValue] ?: 0f
val distance = abs(b - a)
if (distance > 1e-6f) {
val progress = (this.requireOffset() - a) / (b - a)
@@ -229,57 +246,57 @@
private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
- private val velocityThresholdPx = with(density) { velocityThreshold.toPx() }
-
internal val draggableState = DraggableState {
dragPosition = (dragPosition ?: 0f) + it
}
internal var anchors by mutableStateOf(emptyMap<T, Float>())
+ internal var density: Density? = null
+
internal fun updateAnchors(newAnchors: Map<T, Float>) {
val previousAnchorsEmpty = anchors.isEmpty()
anchors = newAnchors
if (previousAnchorsEmpty) {
- dragPosition = anchors.requireAnchor(this.currentState)
+ dragPosition = anchors.requireAnchor(this.currentValue)
}
}
/**
- * Whether the [state] has an anchor associated with it.
+ * Whether the [value] has an anchor associated with it.
*/
- fun hasAnchorForState(state: T): Boolean = anchors.containsKey(state)
+ fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
/**
- * Snap to a [targetState] without any animation.
+ * Snap to a [targetValue] without any animation.
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*
- * @param targetState The target state of the animation
+ * @param targetValue The target value of the animation
*/
- suspend fun snapTo(targetState: T) {
- val targetOffset = anchors.requireAnchor(targetState)
+ suspend fun snapTo(targetValue: T) {
+ val targetOffset = anchors.requireAnchor(targetValue)
draggableState.drag {
dragBy(targetOffset - requireOffset())
}
- this.currentState = targetState
+ this.currentValue = targetValue
}
/**
- * Animate to a [targetState].
+ * Animate to a [targetValue].
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*
- * @param targetState The target state of the animation
+ * @param targetValue The target value of the animation
* @param velocity The velocity the animation should start with, [lastVelocity] by default
*/
suspend fun animateTo(
- targetState: T,
+ targetValue: T,
velocity: Float = lastVelocity,
) {
- val targetOffset = anchors.requireAnchor(targetState)
+ val targetOffset = anchors.requireAnchor(targetValue)
try {
draggableState.drag {
isAnimationRunning = true
@@ -304,7 +321,7 @@
.entries
.firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
?.key
- this.currentState = endState ?: currentState
+ this.currentValue = endState ?: currentValue
}
}
@@ -312,17 +329,17 @@
* Find the closest anchor taking into account the velocity and settle at it with an animation.
*/
suspend fun settle(velocity: Float) {
- val previousState = this.currentState
- val targetState = computeTarget(
+ val previousValue = this.currentValue
+ val targetValue = computeTarget(
offset = requireOffset(),
- currentState = previousState,
+ currentValue = previousValue,
velocity = velocity
)
- if (confirmStateChange(targetState)) {
- animateTo(targetState, velocity)
+ if (confirmValueChange(targetValue)) {
+ animateTo(targetValue, velocity)
} else {
// If the user vetoed the state change, rollback to the previous state.
- animateTo(previousState, velocity)
+ animateTo(previousValue, velocity)
}
}
@@ -344,42 +361,49 @@
private fun computeTarget(
offset: Float,
- currentState: T,
+ currentValue: T,
velocity: Float
): T {
val currentAnchors = anchors
- val currentAnchor = currentAnchors.requireAnchor(currentState)
+ val currentAnchor = currentAnchors.requireAnchor(currentValue)
+ val currentDensity = requireDensity()
+ val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
return if (currentAnchor <= offset) {
// Swiping from lower to upper (positive).
if (velocity >= velocityThresholdPx) {
- currentAnchors.closestState(offset, true)
+ currentAnchors.closestAnchor(offset, true)
} else {
- val upper = currentAnchors.closestState(offset, true)
+ val upper = currentAnchors.closestAnchor(offset, true)
val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
- val relativeThreshold = abs(positionalThreshold(density, distance))
+ val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
val absoluteThreshold = abs(currentAnchor + relativeThreshold)
- if (offset < absoluteThreshold) currentState else upper
+ if (offset < absoluteThreshold) currentValue else upper
}
} else {
// Swiping from upper to lower (negative).
if (velocity <= -velocityThresholdPx) {
- currentAnchors.closestState(offset, false)
+ currentAnchors.closestAnchor(offset, false)
} else {
- val lower = currentAnchors.closestState(offset, false)
+ val lower = currentAnchors.closestAnchor(offset, false)
val distance = abs(currentAnchor - currentAnchors.getValue(lower))
- val relativeThreshold = abs(positionalThreshold(density, distance))
+ val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
val absoluteThreshold = abs(currentAnchor - relativeThreshold)
if (offset < 0) {
// For negative offsets, larger absolute thresholds are closer to lower anchors
// than smaller ones.
- if (abs(offset) < absoluteThreshold) currentState else lower
+ if (abs(offset) < absoluteThreshold) currentValue else lower
} else {
- if (offset > absoluteThreshold) currentState else lower
+ if (offset > absoluteThreshold) currentValue else lower
}
}
}
}
+ private fun requireDensity() = requireNotNull(density) {
+ "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
+ "this=$this SwipeableState?"
+ }
+
companion object {
/**
* The default [Saver] implementation for [SwipeableV2State].
@@ -387,20 +411,18 @@
@ExperimentalMaterial3Api
fun <T : Any> Saver(
animationSpec: AnimationSpec<Float>,
- confirmStateChange: (T) -> Boolean,
+ confirmValueChange: (T) -> Boolean,
positionalThreshold: Density.(distance: Float) -> Float,
- velocityThreshold: Dp,
- density: Density
+ velocityThreshold: Dp
) = Saver<SwipeableV2State<T>, T>(
- save = { it.currentState },
+ save = { it.currentValue },
restore = {
SwipeableV2State(
- initialState = it,
+ initialValue = it,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
- velocityThreshold = velocityThreshold,
- density = density
+ velocityThreshold = velocityThreshold
)
}
)
@@ -410,35 +432,32 @@
/**
* Create and remember a [SwipeableV2State].
*
- * @param initialState The initial state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param initialValue The initial value.
+ * @param animationSpec The default animation that will be used to animate to a new value.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
*/
@Composable
@ExperimentalMaterial3Api
internal fun <T : Any> rememberSwipeableV2State(
- initialState: T,
+ initialValue: T,
animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
- confirmStateChange: (newValue: T) -> Boolean = { true }
+ confirmValueChange: (newValue: T) -> Boolean = { true }
): SwipeableV2State<T> {
- val density = LocalDensity.current
return rememberSaveable(
- initialState, animationSpec, confirmStateChange, density,
+ initialValue, animationSpec, confirmValueChange,
saver = SwipeableV2State.Saver(
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
- density = density
+ velocityThreshold = SwipeableV2Defaults.VelocityThreshold
),
) {
SwipeableV2State(
- initialState = initialState,
+ initialValue = initialValue,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
- density = density
+ velocityThreshold = SwipeableV2Defaults.VelocityThreshold
)
}
}
@@ -492,11 +511,42 @@
fixedPositionalThreshold(56.dp)
}
-private fun <T> Map<T, Float>.closestState(
+@Stable
+private class SwipeAnchorsModifier(
+ private val onDensityChanged: (density: Density) -> Unit,
+ private val onSizeChanged: (layoutSize: IntSize) -> Unit,
+ inspectorInfo: InspectorInfo.() -> Unit,
+) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
+
+ private var lastDensity: Float = -1f
+ private var lastFontScale: Float = -1f
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ if (density != lastDensity || fontScale != lastFontScale) {
+ onDensityChanged(Density(density, fontScale))
+ lastDensity = density
+ lastFontScale = fontScale
+ }
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ }
+
+ override fun onRemeasured(size: IntSize) {
+ onSizeChanged(size)
+ }
+
+ override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
+ "onSizeChanged=$onSizeChanged)"
+}
+
+private fun <T> Map<T, Float>.closestAnchor(
offset: Float = 0f,
searchUpwards: Boolean = false
): T {
- require(isNotEmpty()) { "The anchors were empty when trying to find the closest state" }
+ require(isNotEmpty()) { "The anchors were empty when trying to find the closest anchor" }
return minBy { (_, anchor) ->
val delta = if (searchUpwards) anchor - offset else offset - anchor
if (delta < 0) Float.POSITIVE_INFINITY else delta
@@ -505,6 +555,6 @@
private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(state: T) = requireNotNull(this[state]) {
- "Required anchor $state was not found in anchors. Current anchors: ${this.toMap()}"
+private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
+ "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
}
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index bfc0bbb..692ea89 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -54,11 +54,3 @@
android {
namespace "androidx.compose.runtime.livedata"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/runtime/runtime-rxjava2/build.gradle b/compose/runtime/runtime-rxjava2/build.gradle
index ecf556a8..341374c 100644
--- a/compose/runtime/runtime-rxjava2/build.gradle
+++ b/compose/runtime/runtime-rxjava2/build.gradle
@@ -53,11 +53,3 @@
android {
namespace "androidx.compose.runtime.rxjava2"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/runtime/runtime-rxjava3/build.gradle b/compose/runtime/runtime-rxjava3/build.gradle
index bc209c1..d6ca101 100644
--- a/compose/runtime/runtime-rxjava3/build.gradle
+++ b/compose/runtime/runtime-rxjava3/build.gradle
@@ -53,11 +53,3 @@
android {
namespace "androidx.compose.runtime.rxjava3"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index cf8f78a..2874e46 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -130,11 +130,3 @@
android {
namespace "androidx.compose.runtime.saveable"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/runtime/runtime-tracing/build.gradle b/compose/runtime/runtime-tracing/build.gradle
index 5977424..dffd973 100644
--- a/compose/runtime/runtime-tracing/build.gradle
+++ b/compose/runtime/runtime-tracing/build.gradle
@@ -50,11 +50,3 @@
inceptionYear = "2022"
description = "Additional tracing in Compose"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 69ceccb..0818d02 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -122,11 +122,3 @@
description = "Tree composition support for code generated by the Compose compiler plugin and corresponding public API"
legacyDisableKotlinStrictApiMode = true
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all"
- ]
- }
-}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index e637a6a..79d6d48 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -17,16 +17,22 @@
package androidx.compose.runtime
import androidx.compose.runtime.collection.IdentityArraySet
+import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
import androidx.compose.runtime.snapshots.MutableSnapshot
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotApplyResult
+import androidx.compose.runtime.snapshots.fastAny
import androidx.compose.runtime.snapshots.fastForEach
+import androidx.compose.runtime.snapshots.fastGroupBy
import androidx.compose.runtime.snapshots.fastMap
import androidx.compose.runtime.snapshots.fastMapNotNull
import androidx.compose.runtime.tooling.CompositionData
-import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
-import androidx.compose.runtime.snapshots.fastAny
-import androidx.compose.runtime.snapshots.fastGroupBy
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.resume
+import kotlin.native.concurrent.ThreadLocal
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -44,12 +50,6 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.coroutines.resume
-import kotlin.native.concurrent.ThreadLocal
// TODO: Can we use rootKey for this since all compositions will have an eventual Recomposer parent?
private const val RecomposerCompoundHashKey = 1000
@@ -412,8 +412,14 @@
private fun recordComposerModificationsLocked() {
val changes = snapshotInvalidations
if (changes.isNotEmpty()) {
- knownCompositions.fastForEach { composition ->
- composition.recordModificationsOf(changes)
+ run {
+ knownCompositions.fastForEach { composition ->
+ composition.recordModificationsOf(changes)
+
+ // Avoid using knownCompositions if recording modification detected a
+ // shutdown of the recomposer.
+ if (_state.value <= State.ShuttingDown) return@run
+ }
}
snapshotInvalidations = mutableSetOf()
if (deriveStateLocked() != null) {
@@ -627,6 +633,13 @@
synchronized(stateLock) {
deriveStateLocked()
}
+
+ // Ensure any state objects that were written during apply changes, e.g. nodes
+ // with state-backed properties, get sent apply notifications to invalidate
+ // anything observing the nodes. Call this method instead of
+ // sendApplyNotifications to ensure that objects that were _created_ in this
+ // snapshot are also considered changed after this point.
+ Snapshot.notifyObjectsInitialized()
}
}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 285838a..9c76b09 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -38,10 +38,12 @@
import androidx.compose.runtime.mock.contact
import androidx.compose.runtime.mock.expectChanges
import androidx.compose.runtime.mock.expectNoChanges
+import androidx.compose.runtime.mock.frameDelayMillis
import androidx.compose.runtime.mock.revalidate
import androidx.compose.runtime.mock.skip
import androidx.compose.runtime.mock.validate
import androidx.compose.runtime.snapshots.Snapshot
+import kotlin.coroutines.CoroutineContext
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -53,7 +55,9 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
@Composable
fun Container(content: @Composable () -> Unit) = content()
@@ -3009,7 +3013,8 @@
assertEquals(1, applier.insertCount, "expected setup node not inserted")
shouldEmitNode = false
Snapshot.sendApplyNotifications()
- testScheduler.advanceUntilIdle()
+ // Only advance one frame since the next frame will be automatically scheduled.
+ testScheduler.advanceTimeByFrame(coroutineContext)
assertEquals(1, stateMutatedOnRemove.value, "observable removals performed")
// Only two composition passes should have been performed by this point; a state
// invalidation in the applier should not be picked up or acted upon until after
@@ -3436,18 +3441,32 @@
internal suspend fun <R> localRecomposerTest(
block: CoroutineScope.(Recomposer) -> R
) = coroutineScope {
- val contextWithClock = coroutineContext + TestMonotonicFrameClock(this)
- val recomposer = Recomposer(contextWithClock)
- launch(contextWithClock) {
- recomposer.runRecomposeAndApplyChanges()
+ withContext(TestMonotonicFrameClock(this)) {
+ val recomposer = Recomposer(coroutineContext)
+ launch {
+ recomposer.runRecomposeAndApplyChanges()
+ }
+ block(recomposer)
+ // This call doesn't need to be in a finally; everything it does will be torn down
+ // in exceptional cases by the coroutineScope failure
+ recomposer.cancel()
}
- block(recomposer)
- // This call doesn't need to be in a finally; everything it does will be torn down
- // in exceptional cases by the coroutineScope failure
- recomposer.cancel()
}
-@Composable fun Wrap(content: @Composable () -> Unit) {
+/**
+ * Advances this scheduler by exactly one frame, as defined by the [TestMonotonicFrameClock] in
+ * [context].
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+private fun TestCoroutineScheduler.advanceTimeByFrame(context: CoroutineContext) {
+ val testClock = context[MonotonicFrameClock] as TestMonotonicFrameClock
+ advanceTimeBy(testClock.frameDelayMillis)
+ // advanceTimeBy doesn't run tasks at exactly frameDelayMillis.
+ runCurrent()
+}
+
+@Composable
+fun Wrap(content: @Composable () -> Unit) {
content()
}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt
index 6f594fb..02c15a2 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/EffectsTests.kt
@@ -692,13 +692,16 @@
}
validate(0)
enabler = true
- expectChanges()
- validate(0)
+ // First recomposition here will emit state=0 and create the DisposableEffect. When the
+ // effect runs, it will set state=1, which will invalidate the composition and immediately
+ // trigger another recomposition to emit state=1.
+ assertEquals(2, advanceCount())
+ validate(1)
advance()
- expectChanges()
+ assertEquals(1, advanceCount())
validate(2)
advance()
- expectChanges()
+ assertEquals(1, advanceCount())
validate(3)
}
}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
index 65078df..36f7425 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
@@ -22,21 +22,23 @@
import androidx.compose.runtime.mock.compositionTest
import androidx.compose.runtime.mock.expectNoChanges
import androidx.compose.runtime.snapshots.Snapshot
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
@OptIn(ExperimentalCoroutinesApi::class)
class RecomposerTests {
@@ -309,6 +311,112 @@
fun constructRecomposerWithCancelledJob() {
Recomposer(Job().apply { cancel() })
}
+
+ @Test // regression test for b/243862703
+ fun cancelWithPendingInvalidations() {
+ val dispatcher = StandardTestDispatcher()
+ runTest(dispatcher) {
+ val testClock = TestMonotonicFrameClock(this)
+ withContext(testClock) {
+
+ val recomposer = Recomposer(coroutineContext)
+ var launched = false
+ val runner = launch {
+ launched = true
+ recomposer.runRecomposeAndApplyChanges()
+ }
+ val compositionOne = Composition(UnitApplier(), recomposer)
+ val compositionTwo = Composition(UnitApplier(), recomposer)
+ var state by mutableStateOf(0)
+ var lastCompositionOneState = -1
+ var lastCompositionTwoState = -1
+ compositionOne.setContent {
+ lastCompositionOneState = state
+ LaunchedEffect(Unit) {
+ delay(1_000)
+ }
+ }
+ compositionTwo.setContent {
+ lastCompositionTwoState = state
+ LaunchedEffect(Unit) {
+ delay(1_000)
+ }
+ }
+
+ assertEquals(0, lastCompositionOneState, "initial composition")
+ assertEquals(0, lastCompositionTwoState, "initial composition")
+
+ dispatcher.scheduler.runCurrent()
+
+ assertNotNull(
+ withTimeoutOrNull(3_000) { recomposer.awaitIdle() },
+ "timed out waiting for recomposer idle for recomposition"
+ )
+
+ dispatcher.scheduler.runCurrent()
+
+ assertTrue(launched, "Recomposer was never started")
+
+ Snapshot.withMutableSnapshot {
+ state = 1
+ }
+
+ recomposer.cancel()
+
+ assertNotNull(
+ withTimeoutOrNull(3_000) { recomposer.awaitIdle() },
+ "timed out waiting for recomposer idle for recomposition"
+ )
+
+ assertNotNull(
+ withTimeoutOrNull(3_000) { runner.join() },
+ "timed out waiting for recomposer runner job"
+ )
+ }
+ }
+ }
+
+ @Test
+ fun stateChangesDuringApplyChangesAreNotifiedBeforeFrameFinished() = compositionTest {
+ val count = mutableStateOf(0)
+ val countFromEffect = mutableStateOf(0)
+ val applications = mutableListOf<Set<Any>>()
+ var recompositions = 0
+
+ @Composable
+ fun CountRecorder(count: Int) {
+ SideEffect {
+ countFromEffect.value = count
+ }
+ }
+
+ compose {
+ recompositions++
+ CountRecorder(count.value)
+ }
+
+ assertEquals(0, countFromEffect.value)
+ assertEquals(1, recompositions)
+
+ // Change the count and send the apply notification to invalidate the composition.
+ count.value = 1
+
+ // Register the apply observer after changing state to invalidate composition, but
+ // before actually allowing the recomposition to happen.
+ Snapshot.registerApplyObserver { applied, _ ->
+ applications += applied
+ }
+ assertTrue(applications.isEmpty())
+
+ assertEquals(1, advanceCount())
+
+ // Make sure we actually recomposed.
+ assertEquals(2, recompositions)
+
+ // The Recomposer should have received notification for the node's state.
+ @Suppress("RemoveExplicitTypeArguments")
+ assertEquals<List<Set<Any>>>(listOf(setOf(countFromEffect)), applications)
+ }
}
class UnitApplier : Applier<Unit> {
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 99ccb0c..5b5d26c 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -122,11 +122,3 @@
android {
namespace "androidx.compose.testutils"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index f0c8326..e9cef5f 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -88,11 +88,3 @@
android {
namespace "androidx.compose.ui.geometry"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 5a1dee5..9d729af 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -167,11 +167,3 @@
systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/../../golden"
}
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 690e0fd..f817241 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -103,11 +103,3 @@
inspection {
name = "compose-ui-inspection.jar"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index d7e2dfb..b7ba299 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -157,11 +157,3 @@
description = "Compose testing integration with JUnit4"
legacyDisableKotlinStrictApiMode = true
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-test-manifest/build.gradle b/compose/ui/ui-test-manifest/build.gradle
index 620f03f..6d5972c 100644
--- a/compose/ui/ui-test-manifest/build.gradle
+++ b/compose/ui/ui-test-manifest/build.gradle
@@ -39,11 +39,3 @@
android {
namespace "androidx.compose.ui.test.manifest"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 0863884..641350f 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -166,11 +166,3 @@
description = "Compose testing library"
legacyDisableKotlinStrictApiMode = true
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-text-google-fonts/build.gradle b/compose/ui/ui-text-google-fonts/build.gradle
index 4fb85a7..74702ad 100644
--- a/compose/ui/ui-text-google-fonts/build.gradle
+++ b/compose/ui/ui-text-google-fonts/build.gradle
@@ -52,11 +52,3 @@
android {
namespace "androidx.compose.ui.text.googlefonts"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index a27a205..68fe0b6 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1230,6 +1230,9 @@
public final class AndroidTextPaint_androidKt {
}
+ public final class EmojiCompatStatusKt {
+ }
+
public final class Synchronization_jvmKt {
}
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index c7f1c59..8a1820d 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -370,10 +370,10 @@
method public androidx.compose.ui.text.style.TextGeometricTransform? getTextGeometricTransform();
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.SpanStyle merge(optional androidx.compose.ui.text.SpanStyle? other);
method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.SpanStyle plus(androidx.compose.ui.text.SpanStyle other);
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
property public final long background;
property public final androidx.compose.ui.text.style.BaselineShift? baselineShift;
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
property public final long color;
property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle;
property public final androidx.compose.ui.text.font.FontFamily? fontFamily;
@@ -573,21 +573,21 @@
method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.TextStyle plus(androidx.compose.ui.text.SpanStyle other);
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle toParagraphStyle();
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.SpanStyle toSpanStyle();
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final float alpha;
property public final long background;
property public final androidx.compose.ui.text.style.BaselineShift? baselineShift;
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.Brush? brush;
property public final long color;
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.graphics.drawscope.DrawStyle? drawStyle;
property public final androidx.compose.ui.text.font.FontFamily? fontFamily;
property public final String? fontFeatureSettings;
property public final long fontSize;
property public final androidx.compose.ui.text.font.FontStyle? fontStyle;
property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.Hyphens? hyphens;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.Hyphens? hyphens;
property public final long letterSpacing;
- property @androidx.compose.ui.text.ExperimentalTextApi @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
+ property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
property public final long lineHeight;
property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -1345,6 +1345,9 @@
public final class AndroidTextPaint_androidKt {
}
+ public final class EmojiCompatStatusKt {
+ }
+
public final class Synchronization_jvmKt {
}
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index a27a205..68fe0b6 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1230,6 +1230,9 @@
public final class AndroidTextPaint_androidKt {
}
+ public final class EmojiCompatStatusKt {
+ }
+
public final class Synchronization_jvmKt {
}
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index 49606fa..cc5ecad 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -47,6 +47,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.core:core:1.7.0")
implementation('androidx.collection:collection:1.0.0')
+ implementation("androidx.emoji2:emoji2:1.2.0")
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
@@ -130,6 +131,7 @@
androidMain.dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core:1.7.0")
+ implementation("androidx.emoji2:emoji2:1.2.0")
implementation('androidx.collection:collection:1.0.0')
}
@@ -191,11 +193,3 @@
android {
namespace "androidx.compose.ui.text"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
new file mode 100644
index 0000000..83f7477
--- /dev/null
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsicsTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.platform
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.unit.Density
+import androidx.emoji2.text.EmojiCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class AndroidParagraphIntrinsicsTest {
+
+ val context = InstrumentationRegistry.getInstrumentation().context
+
+ @After
+ fun cleanup() {
+ EmojiCompat.reset(null)
+ EmojiCompatStatus.setDelegateForTesting(null)
+ }
+
+ @Test
+ fun whenEmojiCompatLoads_hasStaleFontsIsTrue() {
+ val fontState = mutableStateOf(false)
+ EmojiCompatStatus.setDelegateForTesting(object : EmojiCompatStatusDelegate {
+ override val fontLoaded: State<Boolean>
+ get() = fontState
+ })
+
+ val subject = ActualParagraphIntrinsics(
+ "text",
+ TextStyle.Default,
+ listOf(),
+ listOf(),
+ Density(1f),
+ createFontFamilyResolver(context)
+ )
+
+ assertThat(subject.hasStaleResolvedFonts).isFalse()
+ fontState.value = true
+ assertThat(subject.hasStaleResolvedFonts).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
new file mode 100644
index 0000000..3d15326
--- /dev/null
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatusTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.platform
+
+import android.graphics.Typeface
+import androidx.compose.runtime.State
+import androidx.emoji2.text.EmojiCompat
+import androidx.emoji2.text.EmojiCompat.LOAD_STRATEGY_MANUAL
+import androidx.emoji2.text.EmojiCompat.MetadataRepoLoader
+import androidx.emoji2.text.MetadataRepo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EmojiCompatStatusTest {
+
+ @Before
+ fun reset() {
+ EmojiCompat.reset(null)
+ EmojiCompatStatus.setDelegateForTesting(null)
+ }
+
+ @After
+ fun clean() {
+ EmojiCompat.reset(null)
+ EmojiCompatStatus.setDelegateForTesting(null)
+ }
+
+ @Test
+ fun nonConfiguredEc_isNotLoaded() {
+ EmojiCompat.reset(null)
+ assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+ }
+
+ @Test
+ fun default_isNotLoaded() {
+ val (config, deferred) = makeEmojiConfig()
+ EmojiCompat.init(config)
+ assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+ deferred.complete(null)
+ }
+
+ @Test
+ fun loading_isNotLoaded() {
+ val (config, deferred) = makeEmojiConfig()
+ val ec = EmojiCompat.init(config)
+ ec.load()
+ assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+ deferred.complete(null)
+ }
+
+ @Test
+ fun error_isNotLoaded() {
+ val (config, deferred) = makeEmojiConfig()
+ val ec = EmojiCompat.init(config)
+ ec.load()
+ deferred.complete(null)
+ assertThat(EmojiCompatStatus.fontLoaded.value).isFalse()
+ }
+
+ @Test
+ fun loaded_isLoaded() {
+ val (config, deferred) = makeEmojiConfig()
+ val ec = EmojiCompat.init(config)
+ deferred.complete(MetadataRepo.create(Typeface.DEFAULT))
+ ec.load()
+ // query now, after init EC
+ EmojiCompatStatus.setDelegateForTesting(null)
+ EmojiCompatStatus.fontLoaded.assertTrue()
+ }
+
+ @Test
+ fun nonLoaded_toLoaded_updatesReturnState() {
+ val (config, deferred) = makeEmojiConfig()
+ val ec = EmojiCompat.init(config)
+ val state = EmojiCompatStatus.fontLoaded
+ assertThat(state.value).isFalse()
+
+ deferred.complete(MetadataRepo.create(Typeface.DEFAULT))
+ ec.load()
+
+ state.assertTrue()
+ }
+
+ @Test
+ fun nonConfigured_canLoadLater() {
+ EmojiCompat.reset(null)
+ val initialFontLoad = EmojiCompatStatus.fontLoaded
+ assertThat(initialFontLoad.value).isFalse()
+
+ val (config, deferred) = makeEmojiConfig()
+ val ec = EmojiCompat.init(config)
+ deferred.complete(MetadataRepo.create(Typeface.DEFAULT))
+ ec.load()
+
+ EmojiCompatStatus.fontLoaded.assertTrue()
+ }
+
+ private fun State<Boolean>.assertTrue() {
+ // there's too many async actors to do anything reasonable and non-flaky here. tie up the
+ // test thread until main posts the value
+ runBlocking {
+ withTimeout(1000) {
+ while (!value)
+ delay(0)
+ }
+ assertThat(value).isTrue()
+ }
+ }
+
+ private fun makeEmojiConfig(): Pair<EmojiCompat.Config, CompletableDeferred<MetadataRepo?>> {
+ val deferred = CompletableDeferred<MetadataRepo?>(null)
+ val loader = MetadataRepoLoader { cb ->
+ CoroutineScope(Dispatchers.Default).launch {
+ val result = deferred.await()
+ if (result != null) {
+ cb.onLoaded(result)
+ } else {
+ cb.onFailed(IllegalStateException("No"))
+ }
+ }
+ }
+ val config = object : EmojiCompat.Config(loader) {}
+ config.setMetadataLoadStrategy(LOAD_STRATEGY_MANUAL)
+ return config to deferred
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
index 9c943d0..3bcd60e 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphHelper.android.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.text.platform
import android.graphics.Typeface
+import android.text.Spannable
import android.text.SpannableString
import android.text.TextPaint
import android.text.style.CharacterStyle
@@ -41,6 +42,7 @@
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.isUnspecified
+import androidx.emoji2.text.EmojiCompat
@OptIn(InternalPlatformTextApi::class, ExperimentalTextApi::class)
internal fun createCharSequence(
@@ -51,16 +53,28 @@
placeholders: List<AnnotatedString.Range<Placeholder>>,
density: Density,
resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface,
+ useEmojiCompat: Boolean,
): CharSequence {
+
+ val currentText = if (useEmojiCompat && EmojiCompat.isConfigured()) {
+ EmojiCompat.get().process(text)!!
+ } else {
+ text
+ }
+
if (spanStyles.isEmpty() &&
placeholders.isEmpty() &&
contextTextStyle.textIndent == TextIndent.None &&
contextTextStyle.lineHeight.isUnspecified
) {
- return text
+ return currentText
}
- val spannableString = SpannableString(text)
+ val spannableString = if (currentText is Spannable) {
+ currentText
+ } else {
+ SpannableString(currentText)
+ }
// b/199939617
// Due to a bug in the platform's native drawText stack, some CJK characters cause a bolder
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
index 8c9a75c..74dd3ce 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraphIntrinsics.android.kt
@@ -65,8 +65,17 @@
private val resolvedTypefaces: MutableList<TypefaceDirtyTracker> = mutableListOf()
+ /**
+ * If emojiCompat is used in the making of this Paragraph
+ *
+ * This value will never change
+ */
+ private val emojiCompatProcessed: Boolean = EmojiCompatStatus.fontLoaded.value
+
override val hasStaleResolvedFonts: Boolean
- get() = resolvedTypefaces.fastAny { it.isStaleResolvedFont }
+ get() = resolvedTypefaces.fastAny { it.isStaleResolvedFont } ||
+ (!emojiCompatProcessed &&
+ /* short-circuit this state read */ EmojiCompatStatus.fontLoaded.value)
internal val textDirectionHeuristic = resolveTextDirectionHeuristics(
style.textDirection,
@@ -74,18 +83,18 @@
)
init {
- val resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface = {
- fontFamily, fontWeight, fontStyle, fontSynthesis ->
- val result = fontFamilyResolver.resolve(
- fontFamily,
- fontWeight,
- fontStyle,
- fontSynthesis
- )
- val holder = TypefaceDirtyTracker(result)
- resolvedTypefaces.add(holder)
- holder.typeface
- }
+ val resolveTypeface: (FontFamily?, FontWeight, FontStyle, FontSynthesis) -> Typeface =
+ { fontFamily, fontWeight, fontStyle, fontSynthesis ->
+ val result = fontFamilyResolver.resolve(
+ fontFamily,
+ fontWeight,
+ fontStyle,
+ fontSynthesis
+ )
+ val holder = TypefaceDirtyTracker(result)
+ resolvedTypefaces.add(holder)
+ holder.typeface
+ }
val notAppliedStyle = textPaint.applySpanStyle(
style = style.toSpanStyle(),
@@ -109,6 +118,7 @@
placeholders = placeholders,
density = density,
resolveTypeface = resolveTypeface,
+ useEmojiCompat = emojiCompatProcessed
)
layoutIntrinsics = LayoutIntrinsics(charSequence, textPaint, textDirectionHeuristic)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.kt
new file mode 100644
index 0000000..b17a3a8
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/EmojiCompatStatus.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.platform
+
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.emoji2.text.EmojiCompat
+
+/**
+ * Tests may provide alternative global implementations for [EmojiCompatStatus] using this delegate.
+ */
+internal interface EmojiCompatStatusDelegate {
+ val fontLoaded: State<Boolean>
+}
+
+/**
+ * Used for observing emojicompat font loading status from compose.
+ */
+internal object EmojiCompatStatus : EmojiCompatStatusDelegate {
+ private var delegate: EmojiCompatStatusDelegate = DefaultImpl()
+
+ /**
+ * True if the emoji2 font is currently loaded and processing will be successful
+ *
+ * False when emoji2 may complete loading in the future.
+ */
+ override val fontLoaded: State<Boolean>
+ get() = delegate.fontLoaded
+
+ /**
+ * Do not call.
+ *
+ * This is for tests that want to control EmojiCompatStatus behavior.
+ */
+ @VisibleForTesting
+ internal fun setDelegateForTesting(newDelegate: EmojiCompatStatusDelegate?) {
+ delegate = newDelegate ?: DefaultImpl()
+ }
+}
+
+/**
+ * is-a state, but doesn't cause an observation when read
+ */
+private class ImmutableBool(override val value: Boolean) : State<Boolean>
+private val Falsey = ImmutableBool(false)
+
+private class DefaultImpl : EmojiCompatStatusDelegate {
+
+ private var loadState: State<Boolean>?
+
+ init {
+ loadState = if (EmojiCompat.isConfigured()) {
+ getFontLoadState()
+ } else {
+ // EC isn't configured yet, will check again in getter
+ null
+ }
+ }
+
+ override val fontLoaded: State<Boolean>
+ get() = if (loadState != null) {
+ loadState!!
+ } else {
+ // EC wasn't configured last time, check again and update loadState if it's ready
+ if (EmojiCompat.isConfigured()) {
+ loadState = getFontLoadState()
+ loadState!!
+ } else {
+ // ec disabled path
+ // no observations allowed, this is pre init
+ Falsey
+ }
+ }
+
+ private fun getFontLoadState(): State<Boolean> {
+ val ec = EmojiCompat.get()
+ return if (ec.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED) {
+ ImmutableBool(true)
+ } else {
+ val mutableLoaded = mutableStateOf(false)
+ val initCallback = object : EmojiCompat.InitCallback() {
+ override fun onInitialized() {
+ mutableLoaded.value = true // update previous observers
+ loadState = ImmutableBool(true) // never observe again
+ }
+
+ override fun onFailed(throwable: Throwable?) {
+ loadState = Falsey // never observe again
+ }
+ }
+ ec.registerInitCallback(initCallback)
+ mutableLoaded
+ }
+ }
+}
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index ec92f46..46505a8 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -131,11 +131,3 @@
android {
namespace "androidx.compose.ui.tooling.data"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index 86c9465..97820f5 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -79,11 +79,3 @@
android {
namespace "androidx.compose.ui.tooling.preview"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index f8affb0..dba7455 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -139,11 +139,3 @@
android {
namespace "androidx.compose.ui.tooling"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index 60360f3..9dbad26 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -117,11 +117,3 @@
android {
namespace "androidx.compose.ui.unit"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index ec838b5..765a47a 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -96,11 +96,3 @@
android {
namespace "androidx.compose.ui.util"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui-viewbinding/build.gradle b/compose/ui/ui-viewbinding/build.gradle
index dca93c8..723ef55 100644
--- a/compose/ui/ui-viewbinding/build.gradle
+++ b/compose/ui/ui-viewbinding/build.gradle
@@ -55,11 +55,3 @@
android {
namespace "androidx.compose.ui.viewbinding"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 3dd72e6..69ffa60 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -473,8 +473,8 @@
property public abstract boolean canFocus;
property public default androidx.compose.ui.focus.FocusRequester down;
property public default androidx.compose.ui.focus.FocusRequester end;
- property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> enter;
- property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> exit;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> enter;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public default kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusDirection,androidx.compose.ui.focus.FocusRequester> exit;
property public default androidx.compose.ui.focus.FocusRequester left;
property public default androidx.compose.ui.focus.FocusRequester next;
property public default androidx.compose.ui.focus.FocusRequester previous;
@@ -1817,12 +1817,12 @@
method public long getUptimeMillis();
method public boolean isConsumed();
property @Deprecated public final androidx.compose.ui.input.pointer.ConsumedData consumed;
- property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public final java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical;
property public final long id;
property public final boolean isConsumed;
property public final long position;
property public final boolean pressed;
- property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public final float pressure;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final float pressure;
property public final long previousPosition;
property public final boolean previousPressed;
property public final long previousUptimeMillis;
@@ -1842,7 +1842,7 @@
method public abstract void onCancel();
method public abstract void onPointerEvent(androidx.compose.ui.input.pointer.PointerEvent pointerEvent, androidx.compose.ui.input.pointer.PointerEventPass pass, long bounds);
property public boolean interceptOutOfBoundsChildEvents;
- property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public boolean shareWithSiblings;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public boolean shareWithSiblings;
property public final long size;
}
@@ -2277,7 +2277,7 @@
method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
- property @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection;
property protected abstract int parentWidth;
}
diff --git a/compose/ui/ui/benchmark/build.gradle b/compose/ui/ui/benchmark/build.gradle
index 0b58476..5cc4f13 100644
--- a/compose/ui/ui/benchmark/build.gradle
+++ b/compose/ui/ui/benchmark/build.gradle
@@ -24,6 +24,7 @@
dependencies {
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.5.1")
androidTestImplementation("androidx.activity:activity-compose:1.3.1")
androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(project(":compose:foundation:foundation"))
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
index ef060ab..082292b 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
@@ -24,8 +24,7 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.createLifecycleAwareWindowRecomposer
import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -49,23 +48,14 @@
@UiThreadTest
fun createRecomposer() {
val rootView = rule.activityTestRule.activity.window.decorView.rootView
- val lifecycle = object : Lifecycle() {
- override fun addObserver(observer: LifecycleObserver) {
- if (observer is LifecycleEventObserver) {
- observer.onStateChanged({ this }, Event.ON_CREATE)
- }
- }
-
- override fun removeObserver(observer: LifecycleObserver) {}
- override fun getCurrentState(): State = State.CREATED
- }
+ val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.CREATED)
var view: View? = null
rule.benchmarkRule.measureRepeated {
runWithTimingDisabled {
view = View(rule.activityTestRule.activity)
(rootView as ViewGroup).addView(view)
}
- view!!.createLifecycleAwareWindowRecomposer(lifecycle = lifecycle)
+ view!!.createLifecycleAwareWindowRecomposer(lifecycle = lifecycleOwner.lifecycle)
runWithTimingDisabled {
(rootView as ViewGroup).removeAllViews()
view = null
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 4944ff2..e44eb1f 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -276,14 +276,6 @@
}
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all"
- ]
- }
-}
-
android {
testOptions.unitTests.includeAndroidResources = true
buildTypes.all {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
index 2d5bf83..ee4701d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
@@ -48,6 +48,7 @@
import androidx.test.espresso.action.Press
import androidx.test.espresso.action.Swipe
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -985,6 +986,7 @@
}
}
+ @FlakyTest(bugId = 259725902)
@Test
fun nestedScroll_movingTarget_velocityShouldRespectSign() {
var lastVelocity = Velocity.Zero
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
new file mode 100644
index 0000000..36a3ef8
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsRealClockTest.kt
@@ -0,0 +1,557 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Copies of most of the tests in [RemeasureWithIntrinsicsTest] but without using
+ * TestMonotonicFrameClock, since it does layout passes slightly differently than in production
+ * and this has bitten us in the past (see b/222093277).
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RemeasureWithIntrinsicsRealClockTest {
+
+ @get:Rule
+ val rule = ActivityScenarioRule(TestActivity::class.java)
+
+ @get:Rule
+ val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
+
+ private val testLatch = CountDownLatch(1)
+
+ @Test
+ fun remeasuringChildWhenParentUsedIntrinsicSizes() {
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var childSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics {
+ LayoutWithIntrinsics(
+ intrinsicWidth,
+ intrinsicHeight,
+ Modifier.onSizeChanged { childSize = it }
+ )
+ }
+ },
+ test = {
+ assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+ withFrameNanos {}
+
+ assertThat(childSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ @Test
+ fun updatingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var childSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics {
+ Box(
+ Modifier
+ .onSizeChanged { childSize = it }
+ .withIntrinsics(intrinsicWidth, intrinsicHeight)
+ )
+ }
+ },
+ test = {
+ assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+ withFrameNanos {}
+
+ assertThat(childSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ @Test
+ fun remeasuringGrandChildWhenGrandParentUsedIntrinsicSizes() {
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var childSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics {
+ Box(propagateMinConstraints = true) {
+ LayoutWithIntrinsics(
+ intrinsicWidth,
+ intrinsicHeight,
+ Modifier.onSizeChanged { childSize = it }
+ )
+ }
+ }
+ },
+ test = {
+ assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+ withFrameNanos {}
+
+ assertThat(childSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ @Test
+ fun updatingGrandChildIntrinsicsViaModifierWhenGrandParentUsedIntrinsicSizes() {
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var childSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics {
+ Box(propagateMinConstraints = true) {
+ Box(
+ Modifier
+ .onSizeChanged { childSize = it }
+ .withIntrinsics(intrinsicWidth, intrinsicHeight)
+ )
+ }
+ }
+ },
+ test = {
+ assertThat(childSize).isEqualTo(IntSize(40, 50))
+
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+ withFrameNanos {}
+
+ assertThat(childSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ @Test
+ fun nodeDoesNotCauseRemeasureOfAncestor_whenItsIntrinsicsAreUnused() {
+ var measures = 0
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var parentSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics(
+ onMeasure = { ++measures },
+ modifier = Modifier.onSizeChanged { parentSize = it }
+ ) {
+ LayoutWithIntrinsics(20, 20) {
+ LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+ }
+ }
+ },
+ test = {
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+
+ withFrameNanos {}
+ assertThat(measures).isEqualTo(1)
+ assertThat(parentSize).isEqualTo(IntSize(20, 20))
+ }
+ )
+ }
+
+ @Test
+ fun causesRemeasureOfAllDependantAncestors() {
+ var measures1 = 0
+ var measures2 = 0
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics(
+ onMeasure = { ++measures1 }
+ ) {
+ Box {
+ LayoutUsingIntrinsics(
+ onMeasure = { ++measures2 }
+ ) {
+ Box {
+ LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+ }
+ }
+ }
+ }
+ },
+ test = {
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+
+ withFrameNanos {}
+ assertThat(measures1).isEqualTo(2)
+ assertThat(measures2).isEqualTo(2)
+
+ // Shouldn't remeasure any more.
+ withFrameNanos {}
+ assertThat(measures1).isEqualTo(2)
+ assertThat(measures2).isEqualTo(2)
+ }
+ )
+ }
+
+ @Test
+ fun whenConnectionFromOwnerDoesNotQueryAnymore() {
+ var measures = 0
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var connectionModifier by mutableStateOf(Modifier as Modifier)
+
+ val parentLayoutPolicy = MeasurePolicy { measurables, constraints ->
+ ++measures
+ val measurable = measurables.first()
+ // Query intrinsics but do not size child to them, to make sure we are
+ // remeasured when the connectionModifier is added.
+ measurable.maxIntrinsicWidth(constraints.maxHeight)
+ measurable.maxIntrinsicHeight(constraints.maxWidth)
+ val placeable = measurable.measure(constraints)
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeable.place(0, 0)
+ }
+ }
+
+ setTestContent(
+ content = {
+ Layout(
+ {
+ Box(modifier = connectionModifier) {
+ LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+ }
+ },
+ measurePolicy = parentLayoutPolicy
+ )
+ },
+ test = {
+ assertThat(measures).isEqualTo(1)
+ connectionModifier = Modifier.size(10.toDp())
+
+ withFrameNanos {}
+ assertThat(measures).isEqualTo(2)
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+
+ withFrameNanos {}
+ assertThat(measures).isEqualTo(2)
+ }
+ )
+ }
+
+ @Test
+ fun whenQueriedFromModifier() {
+ var parentMeasures = 0
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var boxSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutMaybeUsingIntrinsics({ false }, onMeasure = { ++parentMeasures }) {
+ // Box used to fast return intrinsics and do not remeasure when the size
+ // of the inner Box is changing after the intrinsics change.
+ Box(Modifier.requiredSize(100.toDp())) {
+ Box(
+ Modifier
+ .onSizeChanged { boxSize = it }
+ .then(ModifierUsingIntrinsics)
+ ) {
+ LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+ }
+ }
+ }
+ },
+ test = {
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+
+ withFrameNanos {}
+ assertThat(parentMeasures).isEqualTo(1)
+ assertThat(boxSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ @Test
+ fun whenQueriedFromModifier_andAParentQueriesAbove() {
+ var parentMeasures = 0
+ var intrinsicWidth by mutableStateOf(40)
+ var intrinsicHeight by mutableStateOf(50)
+ var boxSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics(onMeasure = { ++parentMeasures }) {
+ // Box used to fast return intrinsics and do not remeasure when the size
+ // of the inner Box is changing after the intrinsics change.
+ Box(Modifier.requiredSize(100.toDp())) {
+ Box(
+ Modifier
+ .onSizeChanged { boxSize = it }
+ .then(ModifierUsingIntrinsics)
+ ) {
+ LayoutWithIntrinsics(intrinsicWidth, intrinsicHeight)
+ }
+ }
+ }
+ },
+ test = {
+ intrinsicWidth = 30
+ intrinsicHeight = 20
+
+ withFrameNanos {}
+ assertThat(parentMeasures).isEqualTo(1)
+ assertThat(boxSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ @Test
+ fun introducingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+ var childModifier by mutableStateOf(Modifier as Modifier)
+ var childSize: IntSize? = null
+
+ setTestContent(
+ content = {
+ LayoutUsingIntrinsics {
+ Box(
+ Modifier
+ .onSizeChanged { childSize = it }
+ .then(childModifier)
+ )
+ }
+ },
+ test = {
+ assertThat(childSize).isEqualTo(IntSize.Zero)
+
+ childModifier = Modifier.withIntrinsics(30, 20)
+
+ withFrameNanos {}
+ assertThat(childSize).isEqualTo(IntSize(30, 20))
+ }
+ )
+ }
+
+ private fun setTestContent(
+ content: @Composable Density.() -> Unit,
+ test: suspend Density.() -> Unit
+ ) {
+ rule.withActivity {
+ setContent {
+ val density = LocalDensity.current
+ content(density)
+ LaunchedEffect(Unit) {
+ // Wait for the first layout pass to finish.
+ withFrameNanos {}
+ test(density)
+ testLatch.countDown()
+ }
+ }
+ }
+ testLatch.await(3, TimeUnit.SECONDS)
+ }
+
+ @Composable
+ private fun LayoutWithIntrinsics(
+ width: Int,
+ height: Int,
+ modifier: Modifier = Modifier,
+ onMeasure: () -> Unit = {},
+ content: @Composable () -> Unit = {}
+ ) {
+ Layout(
+ content = content,
+ modifier = modifier,
+ measurePolicy = object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ onMeasure()
+ return layout(constraints.minWidth, constraints.minHeight) {}
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int = width
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int = height
+ }
+ )
+ }
+
+ @Composable
+ private fun LayoutMaybeUsingIntrinsics(
+ useIntrinsics: () -> Boolean,
+ modifier: Modifier = Modifier,
+ onMeasure: () -> Unit = {},
+ content: @Composable () -> Unit
+ ) {
+ val measurePolicy = remember {
+ object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ require(measurables.size == 1)
+ onMeasure()
+ val childConstraints = if (useIntrinsics()) {
+ val width = measurables.first().maxIntrinsicWidth(constraints.maxHeight)
+ val height = measurables.first().maxIntrinsicHeight(constraints.maxWidth)
+ Constraints.fixed(width, height)
+ } else {
+ constraints
+ }
+ val placeable = measurables.first().measure(childConstraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ) = measurables.first().minIntrinsicWidth(height)
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ) = measurables.first().minIntrinsicHeight(width)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ) = measurables.first().maxIntrinsicWidth(height)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ) = measurables.first().maxIntrinsicHeight(width)
+ }
+ }
+ Layout(content, modifier, measurePolicy)
+ }
+
+ @Composable
+ private fun LayoutUsingIntrinsics(
+ modifier: Modifier = Modifier,
+ onMeasure: () -> Unit = {},
+ content: @Composable () -> Unit
+ ) = LayoutMaybeUsingIntrinsics({ true }, modifier, onMeasure, content)
+
+ private val ModifierUsingIntrinsics = object : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
+ val height = measurable.maxIntrinsicHeight(constraints.maxWidth)
+ val placeable = measurable.measure(Constraints.fixed(width, height))
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ) = measurable.minIntrinsicWidth(height)
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ) = measurable.minIntrinsicHeight(width)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ) = measurable.maxIntrinsicWidth(height)
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ) = measurable.maxIntrinsicHeight(width)
+ }
+
+ private fun Modifier.withIntrinsics(width: Int, height: Int): Modifier {
+ return this.then(object : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = width
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = height
+ })
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
index 86a6f47..5cd91ca 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
@@ -433,154 +433,154 @@
.assertWidthIsEqualTo(30.dp)
.assertHeightIsEqualTo(20.dp)
}
-}
-@Composable
-private fun LayoutWithIntrinsics(
- width: Dp,
- height: Dp,
- modifier: Modifier = Modifier,
- onMeasure: () -> Unit = {},
- content: @Composable () -> Unit = {}
-) {
- Layout(
- content = content,
- modifier = modifier,
- measurePolicy = object : MeasurePolicy {
- override fun MeasureScope.measure(
- measurables: List<Measurable>,
- constraints: Constraints
- ): MeasureResult {
- onMeasure()
- return layout(constraints.minWidth, constraints.minHeight) {}
- }
-
- override fun IntrinsicMeasureScope.maxIntrinsicWidth(
- measurables: List<IntrinsicMeasurable>,
- height: Int
- ): Int = width.roundToPx()
-
- override fun IntrinsicMeasureScope.maxIntrinsicHeight(
- measurables: List<IntrinsicMeasurable>,
- width: Int
- ): Int = height.roundToPx()
- }
- )
-}
-
-@Composable
-private fun LayoutMaybeUsingIntrinsics(
- useIntrinsics: () -> Boolean,
- modifier: Modifier = Modifier,
- onMeasure: () -> Unit = {},
- content: @Composable () -> Unit
-) {
- val measurePolicy = remember {
- object : MeasurePolicy {
- override fun MeasureScope.measure(
- measurables: List<Measurable>,
- constraints: Constraints
- ): MeasureResult {
- require(measurables.size == 1)
- onMeasure()
- val childConstraints = if (useIntrinsics()) {
- val width = measurables.first().maxIntrinsicWidth(constraints.maxHeight)
- val height = measurables.first().maxIntrinsicHeight(constraints.maxWidth)
- Constraints.fixed(width, height)
- } else {
- constraints
+ @Composable
+ private fun LayoutWithIntrinsics(
+ width: Dp,
+ height: Dp,
+ modifier: Modifier = Modifier,
+ onMeasure: () -> Unit = {},
+ content: @Composable () -> Unit = {}
+ ) {
+ Layout(
+ content = content,
+ modifier = modifier,
+ measurePolicy = object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ onMeasure()
+ return layout(constraints.minWidth, constraints.minHeight) {}
}
- val placeable = measurables.first().measure(childConstraints)
- return layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
- }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int = width.roundToPx()
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int = height.roundToPx()
}
-
- override fun IntrinsicMeasureScope.minIntrinsicWidth(
- measurables: List<IntrinsicMeasurable>,
- height: Int
- ) = measurables.first().minIntrinsicWidth(height)
-
- override fun IntrinsicMeasureScope.minIntrinsicHeight(
- measurables: List<IntrinsicMeasurable>,
- width: Int
- ) = measurables.first().minIntrinsicHeight(width)
-
- override fun IntrinsicMeasureScope.maxIntrinsicWidth(
- measurables: List<IntrinsicMeasurable>,
- height: Int
- ) = measurables.first().maxIntrinsicWidth(height)
-
- override fun IntrinsicMeasureScope.maxIntrinsicHeight(
- measurables: List<IntrinsicMeasurable>,
- width: Int
- ) = measurables.first().maxIntrinsicHeight(width)
- }
- }
- Layout(content, modifier, measurePolicy)
-}
-
-@Composable
-private fun LayoutUsingIntrinsics(
- modifier: Modifier = Modifier,
- onMeasure: () -> Unit = {},
- content: @Composable () -> Unit
-) = LayoutMaybeUsingIntrinsics({ true }, modifier, onMeasure, content)
-
-private val ModifierUsingIntrinsics = object : LayoutModifier {
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
- val height = measurable.maxIntrinsicHeight(constraints.maxWidth)
- val placeable = measurable.measure(Constraints.fixed(width, height))
- return layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
- }
+ )
}
- override fun IntrinsicMeasureScope.minIntrinsicWidth(
- measurable: IntrinsicMeasurable,
- height: Int
- ) = measurable.minIntrinsicWidth(height)
+ @Composable
+ private fun LayoutMaybeUsingIntrinsics(
+ useIntrinsics: () -> Boolean,
+ modifier: Modifier = Modifier,
+ onMeasure: () -> Unit = {},
+ content: @Composable () -> Unit
+ ) {
+ val measurePolicy = remember {
+ object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ require(measurables.size == 1)
+ onMeasure()
+ val childConstraints = if (useIntrinsics()) {
+ val width = measurables.first().maxIntrinsicWidth(constraints.maxHeight)
+ val height = measurables.first().maxIntrinsicHeight(constraints.maxWidth)
+ Constraints.fixed(width, height)
+ } else {
+ constraints
+ }
+ val placeable = measurables.first().measure(childConstraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
- override fun IntrinsicMeasureScope.minIntrinsicHeight(
- measurable: IntrinsicMeasurable,
- width: Int
- ) = measurable.minIntrinsicHeight(width)
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ) = measurables.first().minIntrinsicWidth(height)
- override fun IntrinsicMeasureScope.maxIntrinsicWidth(
- measurable: IntrinsicMeasurable,
- height: Int
- ) = measurable.maxIntrinsicWidth(height)
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ) = measurables.first().minIntrinsicHeight(width)
- override fun IntrinsicMeasureScope.maxIntrinsicHeight(
- measurable: IntrinsicMeasurable,
- width: Int
- ) = measurable.maxIntrinsicHeight(width)
-}
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ) = measurables.first().maxIntrinsicWidth(height)
-private fun Modifier.withIntrinsics(width: Dp, height: Dp): Modifier {
- return this.then(object : LayoutModifier {
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ) = measurables.first().maxIntrinsicHeight(width)
+ }
+ }
+ Layout(content, modifier, measurePolicy)
+ }
+
+ @Composable
+ private fun LayoutUsingIntrinsics(
+ modifier: Modifier = Modifier,
+ onMeasure: () -> Unit = {},
+ content: @Composable () -> Unit
+ ) = LayoutMaybeUsingIntrinsics({ true }, modifier, onMeasure, content)
+
+ private val ModifierUsingIntrinsics = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
- val placeable = measurable.measure(constraints)
+ val width = measurable.maxIntrinsicWidth(constraints.maxHeight)
+ val height = measurable.maxIntrinsicHeight(constraints.maxWidth)
+ val placeable = measurable.measure(Constraints.fixed(width, height))
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ) = measurable.minIntrinsicWidth(height)
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ) = measurable.minIntrinsicHeight(width)
+
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
- ): Int = width.roundToPx()
+ ) = measurable.maxIntrinsicWidth(height)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
- ): Int = height.roundToPx()
- })
+ ) = measurable.maxIntrinsicHeight(width)
+ }
+
+ private fun Modifier.withIntrinsics(width: Dp, height: Dp): Modifier {
+ return this.then(object : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = width.roundToPx()
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = height.roundToPx()
+ })
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 02c2c59..65fe4f0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -558,6 +558,7 @@
assertNotNull("test did not run", result?.getOrThrow())
}
+ @Ignore // b/260006789
@Test
fun canScrollVerticallyDown_returnsTrue_onlyAfterDownEventInScrollable() {
lateinit var composeView: View
@@ -583,6 +584,7 @@
}
}
+ @Ignore // b/260006789
@Test
fun canScrollVerticallyUp_returnsTrue_onlyAfterDownEventInScrollable() {
lateinit var composeView: View
@@ -610,6 +612,7 @@
}
}
+ @Ignore // b/260006789
@Test
fun canScrollVertically_returnsFalse_afterDownEventOutsideScrollable() {
lateinit var composeView: View
@@ -633,6 +636,7 @@
}
}
+ @Ignore // b/260006789
@Test
fun canScrollHorizontallyRight_returnsTrue_onlyAfterDownEventInScrollable() {
lateinit var composeView: View
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 0c83242..ef66a60 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.constraintlayout.compose {
- public interface BaselineAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
}
@@ -241,7 +241,7 @@
property public abstract String constraintLayoutTag;
}
- @androidx.compose.runtime.Immutable public interface ConstraintSet {
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
@@ -322,7 +322,7 @@
property public final androidx.constraintlayout.compose.HorizontalAlign Start;
}
- public interface HorizontalAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
}
@@ -466,7 +466,7 @@
property public final androidx.constraintlayout.compose.VerticalAlign Top;
}
- public interface VerticalAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
}
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index 96daef8..dbaa5af 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -32,7 +32,7 @@
property protected final androidx.constraintlayout.core.parser.CLArray framesContainer;
}
- public interface BaselineAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
}
@@ -272,7 +272,7 @@
property public abstract String constraintLayoutTag;
}
- @androidx.compose.runtime.Immutable public interface ConstraintSet {
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
@@ -391,7 +391,7 @@
property public final androidx.constraintlayout.compose.HorizontalAlign Start;
}
- public interface HorizontalAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
}
@@ -887,7 +887,7 @@
property public final androidx.constraintlayout.compose.VerticalAlign Top;
}
- public interface VerticalAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
}
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index d13a833..1295ede 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.constraintlayout.compose {
- public interface BaselineAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
}
@@ -245,7 +245,7 @@
property public abstract String constraintLayoutTag;
}
- @androidx.compose.runtime.Immutable public interface ConstraintSet {
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
@@ -347,7 +347,7 @@
property public final androidx.constraintlayout.compose.HorizontalAlign Start;
}
- public interface HorizontalAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
}
@@ -625,7 +625,7 @@
property public final androidx.constraintlayout.compose.VerticalAlign Top;
}
- public interface VerticalAnchorable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
}
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index c93b13f..e54edbd 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -424,7 +424,7 @@
val boxSize = 100
val offset = 150
- val position = Array(3) { Ref<Offset>() }
+ val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
rule.setContent {
ConstraintLayout(
@@ -441,7 +441,7 @@
}
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[0].value = it.positionInRoot()
+ position[0] = it.positionInRoot().round()
}
)
val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
@@ -453,7 +453,7 @@
}
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[1].value = it.positionInRoot()
+ position[1] = it.positionInRoot().round()
}
)
Box(
@@ -464,7 +464,7 @@
}
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[2].value = it.positionInRoot()
+ position[2] = it.positionInRoot().round()
}
)
}
@@ -476,24 +476,24 @@
rule.runOnIdle {
assertEquals(
Offset(
- ((displayWidth - boxSize) / 2).toFloat(),
- ((displayHeight - boxSize) / 2).toFloat()
- ),
- position[0].value
+ ((displayWidth - boxSize) / 2f),
+ ((displayHeight - boxSize) / 2f)
+ ).round(),
+ position[0]
)
assertEquals(
Offset(
- (displayWidth / 2 + offset).toFloat(),
- ((displayHeight - boxSize) / 2 - boxSize).toFloat()
- ),
- position[1].value
+ (displayWidth / 2f + offset),
+ ((displayHeight - boxSize) / 2f - boxSize)
+ ).round(),
+ position[1]
)
assertEquals(
- Offset(
- offset.toFloat(),
- (displayHeight - boxSize - offset).toFloat()
+ IntOffset(
+ offset,
+ (displayHeight - boxSize - offset)
),
- position[2].value
+ position[2]
)
}
}
@@ -504,7 +504,7 @@
val boxSize = 100
val offset = 150
- val position = Array(3) { Ref<Offset>() }
+ val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
rule.setContent {
ConstraintLayout(
@@ -540,7 +540,7 @@
.layoutId("box$i")
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[i].value = it.positionInRoot()
+ position[i] = it.positionInRoot().round()
}
)
}
@@ -555,22 +555,22 @@
Offset(
(displayWidth - boxSize) / 2f,
(displayHeight - boxSize) / 2f
- ),
- position[0].value
+ ).round(),
+ position[0]
)
assertEquals(
Offset(
(displayWidth / 2f + offset),
- ((displayHeight - boxSize) / 2 - boxSize).toFloat()
- ),
- position[1].value
+ ((displayHeight - boxSize) / 2f - boxSize)
+ ).round(),
+ position[1]
)
assertEquals(
- Offset(
- offset.toFloat(),
- (displayHeight - boxSize - offset).toFloat()
+ IntOffset(
+ offset,
+ (displayHeight - boxSize - offset)
),
- position[2].value
+ position[2]
)
}
}
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
index c97c9ae..c4fe3fa 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
@@ -23,6 +23,7 @@
import androidx.constraintlayout.compose.AnchorFunctions.verticalAnchorFunctions
import androidx.constraintlayout.core.state.ConstraintReference
+@JvmDefaultWithCompatibility
/**
* Represents a vertical side of a layout (i.e start and end) that can be anchored using
* [linkTo] in their `Modifier.constrainAs` blocks.
@@ -38,6 +39,7 @@
)
}
+@JvmDefaultWithCompatibility
/**
* Represents a horizontal side of a layout (i.e top and bottom) that can be anchored using
* [linkTo] in their `Modifier.constrainAs` blocks.
@@ -53,6 +55,7 @@
)
}
+@JvmDefaultWithCompatibility
/**
* Represents the [FirstBaseline] of a layout that can be anchored
* using [linkTo] in their `Modifier.constrainAs` blocks.
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
index c21e716..ff210be 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.constraintlayout.core.state.Transition
+@JvmDefaultWithCompatibility
/**
* Immutable description of the constraints used to layout the children of a [ConstraintLayout].
*/
@@ -38,6 +39,7 @@
fun isDirty(measurables: List<Measurable>): Boolean = true
}
+@JvmDefaultWithCompatibility
@Immutable
internal interface DerivedConstraintSet : ConstraintSet {
/**
diff --git a/core/core-performance/api/current.txt b/core/core-performance/api/current.txt
index 6715c6c..406c06f 100644
--- a/core/core-performance/api/current.txt
+++ b/core/core-performance/api/current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.core.performance {
- public interface DevicePerformance {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
method public default static androidx.core.performance.DevicePerformance create(android.content.Context context);
method public int getMediaPerformanceClass();
property public abstract int mediaPerformanceClass;
diff --git a/core/core-performance/api/public_plus_experimental_current.txt b/core/core-performance/api/public_plus_experimental_current.txt
index 6715c6c..406c06f 100644
--- a/core/core-performance/api/public_plus_experimental_current.txt
+++ b/core/core-performance/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.core.performance {
- public interface DevicePerformance {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
method public default static androidx.core.performance.DevicePerformance create(android.content.Context context);
method public int getMediaPerformanceClass();
property public abstract int mediaPerformanceClass;
diff --git a/core/core-performance/api/restricted_current.txt b/core/core-performance/api/restricted_current.txt
index 6715c6c..406c06f 100644
--- a/core/core-performance/api/restricted_current.txt
+++ b/core/core-performance/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.core.performance {
- public interface DevicePerformance {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface DevicePerformance {
method public default static androidx.core.performance.DevicePerformance create(android.content.Context context);
method public int getMediaPerformanceClass();
property public abstract int mediaPerformanceClass;
diff --git a/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt b/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt
index 874d323..ae4e414 100644
--- a/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt
+++ b/core/core-performance/src/main/java/androidx/core/performance/DevicePerformance.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.os.Build
+@JvmDefaultWithCompatibility
/**
* Reports the media performance class of the device.
*
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 52d84ae..2e29520 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -44,6 +44,7 @@
android {
defaultConfig {
+ minSdkVersion 33
multiDexEnabled = true
}
diff --git a/core/uwb/uwb/api/current.txt b/core/uwb/uwb/api/current.txt
index e7d77f6..34d2568 100644
--- a/core/uwb/uwb/api/current.txt
+++ b/core/uwb/uwb/api/current.txt
@@ -123,7 +123,7 @@
method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
}
- public interface UwbManager {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager {
method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbClientSessionScope>);
method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControleeSessionScope>);
method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControllerSessionScope>);
diff --git a/core/uwb/uwb/api/public_plus_experimental_current.txt b/core/uwb/uwb/api/public_plus_experimental_current.txt
index e7d77f6..34d2568 100644
--- a/core/uwb/uwb/api/public_plus_experimental_current.txt
+++ b/core/uwb/uwb/api/public_plus_experimental_current.txt
@@ -123,7 +123,7 @@
method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
}
- public interface UwbManager {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager {
method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbClientSessionScope>);
method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControleeSessionScope>);
method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControllerSessionScope>);
diff --git a/core/uwb/uwb/api/restricted_current.txt b/core/uwb/uwb/api/restricted_current.txt
index e7d77f6..34d2568 100644
--- a/core/uwb/uwb/api/restricted_current.txt
+++ b/core/uwb/uwb/api/restricted_current.txt
@@ -123,7 +123,7 @@
method public androidx.core.uwb.UwbDevice createForAddress(byte[] address);
}
- public interface UwbManager {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager {
method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbClientSessionScope>);
method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControleeSessionScope>);
method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation<? super androidx.core.uwb.UwbControllerSessionScope>);
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 85d148ce..24a6baa 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -55,6 +55,7 @@
android {
namespace "androidx.core.uwb"
defaultConfig {
+ minSdkVersion 33
multiDexEnabled = true
}
}
diff --git a/core/uwb/uwb/src/main/AndroidManifest.xml b/core/uwb/uwb/src/main/AndroidManifest.xml
index ef5bd56..fd303e0 100644
--- a/core/uwb/uwb/src/main/AndroidManifest.xml
+++ b/core/uwb/uwb/src/main/AndroidManifest.xml
@@ -15,5 +15,7 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
-</manifest>
\ No newline at end of file
+ <queries>
+ <package android:name="androidx.core.uwb.backend" />
+ </queries>
+</manifest>
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
index 62e938d..4167e093 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbComplexChannel.kt
@@ -22,4 +22,10 @@
* @property channel the current channel for the device.
* @property preambleIndex the current preamble index for the device.
*/
-class UwbComplexChannel(val channel: Int, val preambleIndex: Int)
\ No newline at end of file
+class UwbComplexChannel(val channel: Int, val preambleIndex: Int) {
+
+ /** Returns the string format of [UwbComplexChannel]. */
+ override fun toString(): String {
+ return "UwbComplexChannel(channel=$channel, preambleIndex=$preambleIndex)"
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
index e666470..8a0305e 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/UwbManager.kt
@@ -19,6 +19,7 @@
import android.content.Context
import androidx.core.uwb.impl.UwbManagerImpl
+@JvmDefaultWithCompatibility
/**
* Interface for getting UWB capabilities and interacting with nearby UWB devices to perform
* ranging.
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
new file mode 100644
index 0000000..2094c06
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingSessionCallback
+ *
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal", "CallbackMethodName"})
+public interface IRangingSessionCallback extends android.os.IInterface
+{
+ /**
+ * The version of this interface that the caller is built against.
+ * This might be different from what {@link #getInterfaceVersion()
+ * getInterfaceVersion} returns as that is the version of the interface
+ * that the remote object is implementing.
+ */
+ public static final int VERSION = 1;
+ public static final String HASH = "notfrozen";
+ /** Default implementation for IRangingSessionCallback. */
+ public static class Default implements androidx.core.uwb.backend.IRangingSessionCallback
+ {
+ @Override public void onRangingInitialized(@NonNull UwbDevice device) throws android.os.RemoteException
+ {
+ }
+ @Override public void onRangingResult(@NonNull UwbDevice device,
+ @NonNull RangingPosition position) throws android.os.RemoteException
+ {
+ }
+ @Override public void onRangingSuspended(@NonNull UwbDevice device, int reason) throws android.os.RemoteException
+ {
+ }
+ @Override
+ public int getInterfaceVersion() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public String getInterfaceHash() {
+ return "";
+ }
+ @Override
+ @SuppressLint("MissingNullability")
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ @SuppressLint("RawAidl")
+ public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IRangingSessionCallback
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an androidx.core.uwb.backend.IRangingSessionCallback interface,
+ * generating a proxy if needed.
+ */
+ @Nullable
+ public static androidx.core.uwb.backend.IRangingSessionCallback asInterface(
+ @Nullable IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IRangingSessionCallback))) {
+ return ((androidx.core.uwb.backend.IRangingSessionCallback)iin);
+ }
+ return new androidx.core.uwb.backend.IRangingSessionCallback.Stub.Proxy(obj);
+ }
+ @SuppressLint("MissingNullability")
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+ int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ case TRANSACTION_getInterfaceVersion:
+ {
+ reply.writeNoException();
+ reply.writeInt(getInterfaceVersion());
+ return true;
+ }
+ case TRANSACTION_getInterfaceHash:
+ {
+ reply.writeNoException();
+ reply.writeString(getInterfaceHash());
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_onRangingInitialized:
+ {
+ androidx.core.uwb.backend.UwbDevice _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ data.enforceNoDataAvail();
+ this.onRangingInitialized(_arg0);
+ break;
+ }
+ case TRANSACTION_onRangingResult:
+ {
+ androidx.core.uwb.backend.UwbDevice _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ androidx.core.uwb.backend.RangingPosition _arg1;
+ _arg1 = data.readTypedObject(androidx.core.uwb.backend.RangingPosition.CREATOR);
+ data.enforceNoDataAvail();
+ this.onRangingResult(_arg0, _arg1);
+ break;
+ }
+ case TRANSACTION_onRangingSuspended:
+ {
+ androidx.core.uwb.backend.UwbDevice _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ int _arg1;
+ _arg1 = data.readInt();
+ data.enforceNoDataAvail();
+ this.onRangingSuspended(_arg0, _arg1);
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements androidx.core.uwb.backend.IRangingSessionCallback
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ private int mCachedVersion = -1;
+ private String mCachedHash = "-1";
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ @SuppressLint("UnusedMethod")
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void onRangingInitialized(@NonNull androidx.core.uwb.backend.UwbDevice device) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(device, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingInitialized, _data, null, android.os.IBinder.FLAG_ONEWAY);
+ if (!_status) {
+ throw new android.os.RemoteException("Method onRangingInitialized is unimplemented.");
+ }
+ }
+ finally {
+ _data.recycle();
+ }
+ }
+ @Override public void onRangingResult(@NonNull androidx.core.uwb.backend.UwbDevice device, @NonNull androidx.core.uwb.backend.RangingPosition position) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(device, 0);
+ _data.writeTypedObject(position, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingResult, _data, null, android.os.IBinder.FLAG_ONEWAY);
+ if (!_status) {
+ throw new android.os.RemoteException("Method onRangingResult is unimplemented.");
+ }
+ }
+ finally {
+ _data.recycle();
+ }
+ }
+ @Override public void onRangingSuspended(@NonNull androidx.core.uwb.backend.UwbDevice device, int reason) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(device, 0);
+ _data.writeInt(reason);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingSuspended, _data, null, android.os.IBinder.FLAG_ONEWAY);
+ if (!_status) {
+ throw new android.os.RemoteException("Method onRangingSuspended is unimplemented.");
+ }
+ }
+ finally {
+ _data.recycle();
+ }
+ }
+ @Override
+ @SuppressLint("UnusedVariable")
+ public int getInterfaceVersion() throws android.os.RemoteException {
+ if (mCachedVersion == -1) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+ reply.readException();
+ mCachedVersion = reply.readInt();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedVersion;
+ }
+ @NonNull
+ @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+ @Override
+ public synchronized String getInterfaceHash() throws android.os.RemoteException {
+ if ("-1".equals(mCachedHash)) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+ reply.readException();
+ mCachedHash = reply.readString();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedHash;
+ }
+ }
+ static final int TRANSACTION_onRangingInitialized = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_onRangingResult = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_onRangingSuspended = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+ static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+ }
+ public static final java.lang.String DESCRIPTOR = "androidx"
+ + ".core.uwb.backend.IRangingSessionCallback";
+ /** Reasons for suspend */
+ public static final int UNKNOWN = 0;
+ public static final int WRONG_PARAMETERS = 1;
+ public static final int FAILED_TO_START = 2;
+ public static final int STOPPED_BY_PEER = 3;
+ public static final int STOP_RANGING_CALLED = 4;
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_RANGING_ROUND_RETRY_REACHED = 5;
+ public void onRangingInitialized(@NonNull UwbDevice device) throws android.os.RemoteException;
+ public void onRangingResult(@NonNull UwbDevice device, @NonNull RangingPosition position) throws android.os.RemoteException;
+ public void onRangingSuspended(@NonNull UwbDevice device, int reason) throws android.os.RemoteException;
+ @SuppressLint("CallbackMethodName")
+ public int getInterfaceVersion() throws android.os.RemoteException;
+
+ @NonNull
+ @SuppressLint("CallbackMethodName")
+ public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
new file mode 100644
index 0000000..5008f91
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+ /**
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal"})
+public interface IUwb extends android.os.IInterface
+{
+ /**
+ * The version of this interface that the caller is built against.
+ * This might be different from what {@link #getInterfaceVersion()
+ * getInterfaceVersion} returns as that is the version of the interface
+ * that the remote object is implementing.
+ */
+ public static final int VERSION = 1;
+ public static final String HASH = "notfrozen";
+ /** Default implementation for IUwb. */
+ public static class Default implements androidx.core.uwb.backend.IUwb
+ {
+ @NonNull
+ @Override
+ public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException
+ {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override
+ public int getInterfaceVersion() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public String getInterfaceHash() {
+ return "";
+ }
+ @Override
+ @SuppressLint("MissingNullability")
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ @SuppressLint("RawAidl")
+ public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IUwb
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an androidx.core.uwb.backend.IUwb interface,
+ * generating a proxy if needed.
+ */
+ @Nullable
+ public static androidx.core.uwb.backend.IUwb asInterface(@Nullable IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IUwb))) {
+ return ((androidx.core.uwb.backend.IUwb)iin);
+ }
+ return new androidx.core.uwb.backend.IUwb.Stub.Proxy(obj);
+ }
+ @SuppressLint("MissingNullability")
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+ int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ case TRANSACTION_getInterfaceVersion:
+ {
+ reply.writeNoException();
+ reply.writeInt(getInterfaceVersion());
+ return true;
+ }
+ case TRANSACTION_getInterfaceHash:
+ {
+ reply.writeNoException();
+ reply.writeString(getInterfaceHash());
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_getControleeClient:
+ {
+ androidx.core.uwb.backend.IUwbClient _result = this.getControleeClient();
+ reply.writeNoException();
+ reply.writeStrongInterface(_result);
+ break;
+ }
+ case TRANSACTION_getControllerClient:
+ {
+ androidx.core.uwb.backend.IUwbClient _result = this.getControllerClient();
+ reply.writeNoException();
+ reply.writeStrongInterface(_result);
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements androidx.core.uwb.backend.IUwb
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ private int mCachedVersion = -1;
+ private String mCachedHash = "-1";
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ @SuppressLint("UnusedMethod")
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @NonNull
+ @Override public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.IUwbClient _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getControleeClient, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getControleeClient is unimplemented.");
+ }
+ _reply.readException();
+ _result = androidx.core.uwb.backend.IUwbClient.Stub.asInterface(_reply.readStrongBinder());
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @NonNull
+ @Override public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.IUwbClient _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getControllerClient, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getControllerClient is unimplemented.");
+ }
+ _reply.readException();
+ _result = androidx.core.uwb.backend.IUwbClient.Stub.asInterface(_reply.readStrongBinder());
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override
+ @SuppressLint("UnusedVariable")
+ public int getInterfaceVersion() throws android.os.RemoteException {
+ if (mCachedVersion == -1) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+ reply.readException();
+ mCachedVersion = reply.readInt();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedVersion;
+ }
+ @NonNull
+ @Override
+ @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+ public synchronized String getInterfaceHash() throws android.os.RemoteException {
+ if ("-1".equals(mCachedHash)) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+ reply.readException();
+ mCachedHash = reply.readString();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedHash;
+ }
+ }
+ static final int TRANSACTION_getControleeClient = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_getControllerClient = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+ static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+ }
+ public static final java.lang.String DESCRIPTOR =
+ "androidx.core.uwb.backend.IUwb";
+
+ @NonNull
+ public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException;
+
+ @NonNull
+ public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException;
+ public int getInterfaceVersion() throws android.os.RemoteException;
+
+ @NonNull
+ public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
new file mode 100644
index 0000000..e9f53ea
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbClient
+ *
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal", "ExecutorRegistration"})
+public interface IUwbClient extends android.os.IInterface
+{
+ /**
+ * The version of this interface that the caller is built against.
+ * This might be different from what {@link #getInterfaceVersion()
+ * getInterfaceVersion} returns as that is the version of the interface
+ * that the remote object is implementing.
+ */
+ public static final int VERSION = 1;
+ public static final String HASH = "notfrozen";
+ /** Default implementation for IUwbClient. */
+ public static class Default implements androidx.core.uwb.backend.IUwbClient
+ {
+ @Override public boolean isAvailable() throws android.os.RemoteException
+ {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Nullable
+ @Override public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Nullable
+ @Override public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @SuppressLint("ExecutorRegistration")
+ @Override public void startRanging(@NonNull RangingParameters parameters,
+ @NonNull IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ }
+ @SuppressLint("ExecutorRegistration")
+ @Override public void stopRanging(@NonNull IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ }
+ @Override public void addControlee(@NonNull UwbAddress address) throws android.os.RemoteException
+ {
+ }
+ @Override public void removeControlee(@NonNull UwbAddress address) throws android.os.RemoteException
+ {
+ }
+ @Override
+ public int getInterfaceVersion() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public String getInterfaceHash() {
+ return "";
+ }
+ @Override
+ @SuppressLint("MissingNullability")
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ @SuppressLint("RawAidl")
+ public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IUwbClient
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an androidx.core.uwb.backend.IUwbClient interface,
+ * generating a proxy if needed.
+ */
+ @Nullable
+ public static androidx.core.uwb.backend.IUwbClient asInterface(@Nullable IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IUwbClient))) {
+ return ((androidx.core.uwb.backend.IUwbClient)iin);
+ }
+ return new androidx.core.uwb.backend.IUwbClient.Stub.Proxy(obj);
+ }
+ @SuppressLint("MissingNullability")
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+ int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ case TRANSACTION_getInterfaceVersion:
+ {
+ reply.writeNoException();
+ reply.writeInt(getInterfaceVersion());
+ return true;
+ }
+ case TRANSACTION_getInterfaceHash:
+ {
+ reply.writeNoException();
+ reply.writeString(getInterfaceHash());
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_isAvailable:
+ {
+ boolean _result = this.isAvailable();
+ reply.writeNoException();
+ reply.writeBoolean(_result);
+ break;
+ }
+ case TRANSACTION_getRangingCapabilities:
+ {
+ androidx.core.uwb.backend.RangingCapabilities _result = this.getRangingCapabilities();
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getLocalAddress:
+ {
+ androidx.core.uwb.backend.UwbAddress _result = this.getLocalAddress();
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getComplexChannel:
+ {
+ androidx.core.uwb.backend.UwbComplexChannel _result = this.getComplexChannel();
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_startRanging:
+ {
+ androidx.core.uwb.backend.RangingParameters _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.RangingParameters.CREATOR);
+ androidx.core.uwb.backend.IRangingSessionCallback _arg1;
+ _arg1 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
+ data.enforceNoDataAvail();
+ this.startRanging(_arg0, _arg1);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_stopRanging:
+ {
+ androidx.core.uwb.backend.IRangingSessionCallback _arg0;
+ _arg0 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
+ data.enforceNoDataAvail();
+ this.stopRanging(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_addControlee:
+ {
+ androidx.core.uwb.backend.UwbAddress _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ data.enforceNoDataAvail();
+ this.addControlee(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_removeControlee:
+ {
+ androidx.core.uwb.backend.UwbAddress _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ data.enforceNoDataAvail();
+ this.removeControlee(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements androidx.core.uwb.backend.IUwbClient
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ private int mCachedVersion = -1;
+ private String mCachedHash = "-1";
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ @SuppressLint("UnusedMethod")
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public boolean isAvailable() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ boolean _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_isAvailable, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method isAvailable is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readBoolean();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @NonNull
+ @Override public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.RangingCapabilities _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getRangingCapabilities, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getRangingCapabilities is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readTypedObject(androidx.core.uwb.backend.RangingCapabilities.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.UwbAddress _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getLocalAddress, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getLocalAddress is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.UwbComplexChannel _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getComplexChannel, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getComplexChannel is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readTypedObject(androidx.core.uwb.backend.UwbComplexChannel.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public void startRanging(@NonNull androidx.core.uwb.backend.RangingParameters parameters, @NonNull androidx.core.uwb.backend.IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(parameters, 0);
+ _data.writeStrongInterface(callback);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_startRanging, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method startRanging is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void stopRanging(@NonNull androidx.core.uwb.backend.IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongInterface(callback);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_stopRanging, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method stopRanging is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void addControlee(@NonNull androidx.core.uwb.backend.UwbAddress address) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(address, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_addControlee, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method addControlee is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void removeControlee(@NonNull androidx.core.uwb.backend.UwbAddress address) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(address, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_removeControlee, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method removeControlee is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override
+ @SuppressLint("UnusedVariable")
+ public int getInterfaceVersion() throws android.os.RemoteException {
+ if (mCachedVersion == -1) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+ reply.readException();
+ mCachedVersion = reply.readInt();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedVersion;
+ }
+ @NonNull
+ @Override
+ @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+ public synchronized String getInterfaceHash() throws android.os.RemoteException {
+ if ("-1".equals(mCachedHash)) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+ reply.readException();
+ mCachedHash = reply.readString();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedHash;
+ }
+ }
+ static final int TRANSACTION_isAvailable = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_getRangingCapabilities = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_getLocalAddress = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ static final int TRANSACTION_getComplexChannel = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
+ static final int TRANSACTION_startRanging = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
+ static final int TRANSACTION_stopRanging = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
+ static final int TRANSACTION_addControlee = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6);
+ static final int TRANSACTION_removeControlee = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7);
+ static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+ static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+ }
+ public static final java.lang.String DESCRIPTOR =
+ "androidx.core.uwb.backend.IUwbClient";
+ public boolean isAvailable() throws android.os.RemoteException;
+
+ @Nullable
+ public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException;
+
+ @Nullable
+ public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException;
+
+ @Nullable
+ public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException;
+ @SuppressLint("ExecutorRegistration")
+ public void startRanging(@NonNull RangingParameters parameters,
+ @NonNull IRangingSessionCallback callback) throws android.os.RemoteException;
+ @SuppressLint("ExecutorRegistration")
+ public void stopRanging(@NonNull IRangingSessionCallback callback) throws android.os.RemoteException;
+ public void addControlee(@NonNull UwbAddress address) throws android.os.RemoteException;
+ public void removeControlee(@NonNull UwbAddress address) throws android.os.RemoteException;
+ public int getInterfaceVersion() throws android.os.RemoteException;
+
+ @NonNull
+ public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
new file mode 100644
index 0000000..6da98f1
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingCapabilities
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingCapabilities implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public boolean supportsDistance = false;
+ @SuppressLint("MutableBareField")
+ public boolean supportsAzimuthalAngle = false;
+ @SuppressLint("MutableBareField")
+ public boolean supportsElevationAngle = false;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingCapabilities> CREATOR = new android.os.Parcelable.Creator<RangingCapabilities>() {
+ @Override
+ public RangingCapabilities createFromParcel(android.os.Parcel _aidl_source) {
+ RangingCapabilities _aidl_out = new RangingCapabilities();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingCapabilities[] newArray(int _aidl_size) {
+ return new RangingCapabilities[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeBoolean(supportsDistance);
+ _aidl_parcel.writeBoolean(supportsAzimuthalAngle);
+ _aidl_parcel.writeBoolean(supportsElevationAngle);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ supportsDistance = _aidl_parcel.readBoolean();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ supportsAzimuthalAngle = _aidl_parcel.readBoolean();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ supportsElevationAngle = _aidl_parcel.readBoolean();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
new file mode 100644
index 0000000..5b02460
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingMeasurement
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingMeasurement implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public int confidence = 0;
+ @SuppressLint("MutableBareField")
+ public float value = 0.000000f;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingMeasurement> CREATOR = new android.os.Parcelable.Creator<RangingMeasurement>() {
+ @Override
+ public RangingMeasurement createFromParcel(android.os.Parcel _aidl_source) {
+ RangingMeasurement _aidl_out = new RangingMeasurement();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingMeasurement[] newArray(int _aidl_size) {
+ return new RangingMeasurement[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(confidence);
+ _aidl_parcel.writeFloat(value);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ confidence = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ value = _aidl_parcel.readFloat();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
new file mode 100644
index 0000000..966830c
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingParameters
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingParameters implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public int uwbConfigId;
+ @SuppressLint("MutableBareField")
+ public int sessionId = 0;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public byte[] sessionKeyInfo;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.UwbComplexChannel complexChannel;
+ @Nullable
+ @SuppressLint({"MutableBareField", "NullableCollection"})
+ public java.util.List<androidx.core.uwb.backend.UwbDevice> peerDevices;
+ @SuppressLint("MutableBareField")
+ public int rangingUpdateRate = 0;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingParameters> CREATOR = new android.os.Parcelable.Creator<RangingParameters>() {
+ @Override
+ public RangingParameters createFromParcel(android.os.Parcel _aidl_source) {
+ RangingParameters _aidl_out = new RangingParameters();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingParameters[] newArray(int _aidl_size) {
+ return new RangingParameters[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(uwbConfigId);
+ _aidl_parcel.writeInt(sessionId);
+ _aidl_parcel.writeByteArray(sessionKeyInfo);
+ _aidl_parcel.writeTypedObject(complexChannel, _aidl_flag);
+ _aidl_parcel.writeTypedList(peerDevices);
+ _aidl_parcel.writeInt(rangingUpdateRate);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ uwbConfigId = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ sessionId = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ sessionKeyInfo = _aidl_parcel.createByteArray();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ complexChannel = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.UwbComplexChannel.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ peerDevices = _aidl_parcel.createTypedArrayList(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ rangingUpdateRate = _aidl_parcel.readInt();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(complexChannel);
+ _mask |= describeContents(peerDevices);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof java.util.Collection) {
+ int _mask = 0;
+ for (Object o : (java.util.Collection) _v) {
+ _mask |= describeContents(o);
+ }
+ return _mask;
+ }
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
new file mode 100644
index 0000000..1173b81
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingPosition
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingPosition implements android.os.Parcelable
+{
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.RangingMeasurement distance;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.RangingMeasurement azimuth;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.RangingMeasurement elevation;
+ @SuppressLint("MutableBareField")
+ public long elapsedRealtimeNanos = 0L;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingPosition> CREATOR = new android.os.Parcelable.Creator<RangingPosition>() {
+ @Override
+ public RangingPosition createFromParcel(android.os.Parcel _aidl_source) {
+ RangingPosition _aidl_out = new RangingPosition();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingPosition[] newArray(int _aidl_size) {
+ return new RangingPosition[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeTypedObject(distance, _aidl_flag);
+ _aidl_parcel.writeTypedObject(azimuth, _aidl_flag);
+ _aidl_parcel.writeTypedObject(elevation, _aidl_flag);
+ _aidl_parcel.writeLong(elapsedRealtimeNanos);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ distance = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ azimuth = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ elevation = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ elapsedRealtimeNanos = _aidl_parcel.readLong();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(distance);
+ _mask |= describeContents(azimuth);
+ _mask |= describeContents(elevation);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
new file mode 100644
index 0000000..e076cd1
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbAddress
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbAddress implements android.os.Parcelable
+{
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public byte[] address;
+ @NonNull
+ public static final android.os.Parcelable.Creator<UwbAddress> CREATOR = new android.os.Parcelable.Creator<UwbAddress>() {
+ @Override
+ public UwbAddress createFromParcel(android.os.Parcel _aidl_source) {
+ UwbAddress _aidl_out = new UwbAddress();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public UwbAddress[] newArray(int _aidl_size) {
+ return new UwbAddress[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeByteArray(address);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ address = _aidl_parcel.createByteArray();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
new file mode 100644
index 0000000..24d001a
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbComplexChannel
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbComplexChannel implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public int channel = 0;
+ @SuppressLint("MutableBareField")
+ public int preambleIndex = 0;
+ @NonNull
+ public static final android.os.Parcelable.Creator<UwbComplexChannel> CREATOR = new android.os.Parcelable.Creator<UwbComplexChannel>() {
+ @Override
+ public UwbComplexChannel createFromParcel(android.os.Parcel _aidl_source) {
+ UwbComplexChannel _aidl_out = new UwbComplexChannel();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public UwbComplexChannel[] newArray(int _aidl_size) {
+ return new UwbComplexChannel[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(channel);
+ _aidl_parcel.writeInt(preambleIndex);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ channel = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ preambleIndex = _aidl_parcel.readInt();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
new file mode 100644
index 0000000..054aad7
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbDevice
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbDevice implements android.os.Parcelable
+{
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.UwbAddress address;
+ @NonNull
+ public static final android.os.Parcelable.Creator<UwbDevice> CREATOR = new android.os.Parcelable.Creator<UwbDevice>() {
+ @Override
+ public UwbDevice createFromParcel(android.os.Parcel _aidl_source) {
+ UwbDevice _aidl_out = new UwbDevice();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public UwbDevice[] newArray(int _aidl_size) {
+ return new UwbDevice[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeTypedObject(address, _aidl_flag);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ address = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(address);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt
new file mode 100644
index 0000000..fea81c2
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.uwb.impl
+
+import android.util.Log
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingMeasurement
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.backend.IRangingSessionCallback
+import androidx.core.uwb.backend.IUwbClient
+import androidx.core.uwb.backend.RangingPosition
+import androidx.core.uwb.backend.UwbComplexChannel
+import androidx.core.uwb.backend.UwbDevice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+internal class UwbClientSessionScopeAospImpl(
+ private val uwbClient: IUwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress,
+) : UwbControleeSessionScope {
+ companion object {
+ private const val TAG = "UwbClientSessionScope"
+ }
+ private var sessionStarted = false
+
+ override fun prepareSession(parameters: RangingParameters) = callbackFlow {
+ if (sessionStarted) {
+ throw IllegalStateException("Ranging has already started. To initiate " +
+ "a new ranging session, create a new client session scope.")
+ }
+
+ val parametersBuilder1 = androidx.core.uwb.backend.RangingParameters()
+ parametersBuilder1.uwbConfigId = when (parameters.uwbConfigType) {
+ RangingParameters.UWB_CONFIG_ID_1 -> RangingParameters.UWB_CONFIG_ID_1
+ RangingParameters.UWB_CONFIG_ID_3 -> RangingParameters.UWB_CONFIG_ID_3
+ else -> throw IllegalArgumentException("The selected UWB Config Id is not a valid id.")
+ }
+ parametersBuilder1.rangingUpdateRate = when (parameters.updateRateType) {
+ RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC ->
+ RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC
+ RangingParameters.RANGING_UPDATE_RATE_FREQUENT ->
+ RangingParameters.RANGING_UPDATE_RATE_FREQUENT
+ RangingParameters.RANGING_UPDATE_RATE_INFREQUENT ->
+ RangingParameters.RANGING_UPDATE_RATE_INFREQUENT
+ else -> throw IllegalArgumentException(
+ "The selected ranging update rate is not a valid update rate.")
+ }
+ parametersBuilder1.sessionId = parameters.sessionId
+ parametersBuilder1.sessionKeyInfo = parameters.sessionKeyInfo
+ if (parameters.complexChannel != null) {
+ val channel = UwbComplexChannel()
+ channel.channel = parameters.complexChannel.channel
+ channel.preambleIndex = parameters.complexChannel.preambleIndex
+ parametersBuilder1.complexChannel = channel
+ }
+
+ val peerList = ArrayList<UwbDevice>()
+ for (peer in parameters.peerDevices) {
+ val device = UwbDevice()
+ val address = androidx.core.uwb.backend.UwbAddress()
+ address.address = peer.address.address
+ device.address = address
+ peerList.add(device)
+ }
+ parametersBuilder1.peerDevices = peerList
+ val callback =
+ object : IRangingSessionCallback.Stub() {
+ override fun onRangingInitialized(device: UwbDevice) {
+ Log.i(TAG, "Started UWB ranging.")
+ }
+
+ override fun onRangingResult(device: UwbDevice, position: RangingPosition) {
+ trySend(
+ RangingResult.RangingResultPosition(
+ androidx.core.uwb.UwbDevice(UwbAddress(device.address?.address!!)),
+ androidx.core.uwb.RangingPosition(
+ position.distance?.let { RangingMeasurement(it.value) },
+ position.azimuth?.let {
+ RangingMeasurement(it.value)
+ },
+ position.elevation?.let {
+ RangingMeasurement(it.value)
+ },
+ position.elapsedRealtimeNanos
+ )
+ )
+ )
+ }
+
+ override fun onRangingSuspended(device: UwbDevice, reason: Int) {
+ trySend(
+ RangingResult.RangingResultPeerDisconnected(
+ androidx.core.uwb.UwbDevice(UwbAddress(device.address?.address!!))
+ )
+ )
+ }
+
+ override fun getInterfaceVersion(): Int {
+ return 0
+ }
+
+ override fun getInterfaceHash(): String {
+ return ""
+ }
+ }
+
+ try {
+ uwbClient.startRanging(parametersBuilder1, callback)
+ sessionStarted = true
+ } catch (e: Exception) {
+ throw(e)
+ }
+
+ awaitClose {
+ CoroutineScope(Dispatchers.Main.immediate).launch {
+ try {
+ uwbClient.stopRanging(callback)
+ } catch (e: Exception) {
+ throw(e)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt
new file mode 100644
index 0000000..e39bed3
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.uwb.impl
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.backend.IUwbClient
+import kotlinx.coroutines.flow.Flow
+
+internal class UwbControleeSessionScopeAospImpl(
+ uwbClient: IUwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress
+) : UwbControleeSessionScope {
+ private val uwbClientSessionScope =
+ UwbClientSessionScopeAospImpl(uwbClient, rangingCapabilities, localAddress)
+
+ override fun prepareSession(parameters: RangingParameters): Flow<RangingResult> {
+ return uwbClientSessionScope.prepareSession(parameters)
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt
new file mode 100644
index 0000000..0630d41
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.uwb.impl
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbComplexChannel
+import androidx.core.uwb.UwbControllerSessionScope
+import androidx.core.uwb.backend.IUwbClient
+import kotlinx.coroutines.flow.Flow
+
+internal class UwbControllerSessionScopeAospImpl(
+ private val uwbClient: IUwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress,
+ override val uwbComplexChannel: UwbComplexChannel
+) : UwbControllerSessionScope {
+ private val uwbClientSessionScope =
+ UwbClientSessionScopeAospImpl(uwbClient, rangingCapabilities, localAddress)
+ override suspend fun addControlee(address: UwbAddress) {
+ val uwbAddress = androidx.core.uwb.backend.UwbAddress()
+ uwbAddress.address = address.address
+ try {
+ uwbClient.addControlee(uwbAddress)
+ } catch (e: Exception) {
+ throw(e)
+ }
+ }
+
+ override suspend fun removeControlee(address: UwbAddress) {
+ val uwbAddress = androidx.core.uwb.backend.UwbAddress()
+ uwbAddress.address = address.address
+ try {
+ uwbClient.removeControlee(uwbAddress)
+ } catch (e: Exception) {
+ throw(e)
+ }
+ }
+
+ override fun prepareSession(parameters: RangingParameters): Flow<RangingResult> {
+ return uwbClientSessionScope.prepareSession(parameters)
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
index bbf9cec..84297cf 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
@@ -16,7 +16,12 @@
package androidx.core.uwb.impl
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
import androidx.core.uwb.RangingCapabilities
import androidx.core.uwb.UwbAddress
import androidx.core.uwb.UwbClientSessionScope
@@ -24,13 +29,35 @@
import androidx.core.uwb.UwbControleeSessionScope
import androidx.core.uwb.UwbControllerSessionScope
import androidx.core.uwb.UwbManager
+import androidx.core.uwb.backend.IUwb
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.nearby.Nearby
import kotlinx.coroutines.tasks.await
import androidx.core.uwb.helper.checkSystemFeature
import androidx.core.uwb.helper.handleApiException
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
internal class UwbManagerImpl(private val context: Context) : UwbManager {
+ companion object {
+ const val TAG = "UwbMangerImpl"
+ var iUwb: IUwb? = null
+ }
+
+ init {
+ val connection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ iUwb = IUwb.Stub.asInterface(service)
+ Log.i(TAG, "iUwb service created successfully.")
+ }
+ override fun onServiceDisconnected(p0: ComponentName?) {
+ iUwb = null
+ }
+ }
+ val intent = Intent("androidx.core.uwb.backend.service")
+ intent.setPackage("androidx.core.uwb.backend")
+ context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ }
@Deprecated("Renamed to controleeSessionScope")
override suspend fun clientSessionScope(): UwbClientSessionScope {
return createClientSessionScope(false)
@@ -46,6 +73,14 @@
private suspend fun createClientSessionScope(isController: Boolean): UwbClientSessionScope {
checkSystemFeature(context)
+ val hasGmsCore = GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+ return if (hasGmsCore) createGmsClientSessionScope(isController)
+ else createAospClientSessionScope(isController)
+ }
+
+ private suspend fun createGmsClientSessionScope(isController: Boolean): UwbClientSessionScope {
+ Log.i(TAG, "Creating Gms Client session scope")
val uwbClient = if (isController)
Nearby.getUwbControllerClient(context) else Nearby.getUwbControleeClient(context)
try {
@@ -77,4 +112,41 @@
"up-to-date with the service backend.")
}
}
+
+ private fun createAospClientSessionScope(isController: Boolean): UwbClientSessionScope {
+ Log.i(TAG, "Creating Aosp Client session scope")
+ val uwbClient = if (isController)
+ iUwb?.controllerClient else iUwb?.controleeClient
+ if (uwbClient == null) {
+ Log.e(TAG, "Failed to get UwbClient. AOSP backend is not available.")
+ }
+ try {
+ val aospLocalAddress = uwbClient!!.localAddress
+ val aospRangingCapabilities = uwbClient.rangingCapabilities
+ val localAddress = aospLocalAddress?.address?.let { UwbAddress(it) }
+ val rangingCapabilities = aospRangingCapabilities?.let {
+ RangingCapabilities(
+ it.supportsDistance,
+ it.supportsAzimuthalAngle,
+ it.supportsElevationAngle)
+ }
+ return if (isController) {
+ val uwbComplexChannel = uwbClient.complexChannel
+ UwbControllerSessionScopeAospImpl(
+ uwbClient,
+ rangingCapabilities!!,
+ localAddress!!,
+ UwbComplexChannel(uwbComplexChannel!!.channel, uwbComplexChannel.preambleIndex)
+ )
+ } else {
+ UwbControleeSessionScopeAospImpl(
+ uwbClient,
+ rangingCapabilities!!,
+ localAddress!!
+ )
+ }
+ } catch (e: Exception) {
+ throw e
+ }
+ }
}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index a3e6a53..e5e59bf 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -32,6 +32,7 @@
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
import java.util.concurrent.Executor
@@ -61,9 +62,13 @@
if (cancellationReviewer(fragmentManager, cancellationSignal)) {
return
}
- TODO("Not yet implemented")
+ // TODO("Manage Fragment Lifecycle and Fragment Manager Properly")
+ CredentialProviderBeginSignInController.getInstance(fragmentManager).invokePlayServices(
+ request, callback, executor
+ )
}
+ @SuppressWarnings("deprecated")
override fun onCreateCredential(
request: CreateCredentialRequest,
activity: Activity?,
@@ -88,12 +93,15 @@
callback,
executor)
} else if (request is CreatePublicKeyCredentialRequest) {
- TODO("Not yet implemented")
+ // TODO("Add in")
} else {
throw UnsupportedOperationException(
"Unsupported request; not password or publickeycredential")
}
}
+ override fun isAvailableOnDevice(): Boolean {
+ TODO("Not yet implemented")
+ }
@SuppressLint("ClassVerificationFailure", "NewApi")
private fun cancellationReviewer(
@@ -116,10 +124,6 @@
return false
}
- override fun isAvailableOnDevice(): Boolean {
- TODO("Not yet implemented")
- }
-
companion object {
private val TAG = CredentialProviderPlayServicesImpl::class.java.name
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
new file mode 100644
index 0000000..83f8738
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 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.credentials.playservices.controllers.BeginSignIn
+
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetPasswordOption
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+
+/**
+ * A utility class to handle logic for the begin sign in controller.
+ *
+ * @hide
+ */
+class BeginSignInControllerUtility {
+
+ companion object {
+ internal fun constructBeginSignInRequest(request: GetCredentialRequest):
+ BeginSignInRequest {
+ val requestBuilder = BeginSignInRequest.Builder()
+ for (option in request.getCredentialOptions) {
+ if (option is GetPasswordOption) {
+ requestBuilder.setPasswordRequestOptions(
+ BeginSignInRequest.PasswordRequestOptions.Builder()
+ .setSupported(true)
+ .build()
+ )
+ }
+ // TODO("Add GoogleIDToken version and passkey version")
+ }
+ return requestBuilder
+ .setAutoSelectEnabled(request.isAutoSelectAllowed)
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
index e38cca7..1c9f3c1 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -16,15 +16,29 @@
package androidx.credentials.playservices.controllers.BeginSignIn
+import android.annotation.SuppressLint
+import android.app.Activity
import android.content.Intent
+import android.content.IntentSender
+import android.os.Bundle
import android.util.Log
+import androidx.credentials.Credential
import androidx.credentials.CredentialManagerCallback
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
+import androidx.credentials.PasswordCredential
+import androidx.credentials.exceptions.GetCredentialCanceledException
import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.playservices.controllers.BeginSignIn.BeginSignInControllerUtility.Companion.constructBeginSignInRequest
import androidx.credentials.playservices.controllers.CredentialProviderController
import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.BeginSignInResult
+import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.identity.SignInCredential
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes
import java.util.concurrent.Executor
/**
@@ -50,12 +64,54 @@
*/
private lateinit var executor: Executor
+ @SuppressLint("ClassVerificationFailure", "NewApi")
override fun invokePlayServices(
request: GetCredentialRequest,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
executor: Executor
) {
- TODO("Not yet implemented")
+ this.callback = callback
+ this.executor = executor
+ val convertedRequest: BeginSignInRequest = this.convertRequestToPlayServices(request)
+ Identity.getSignInClient(activity)
+ .beginSignIn(convertedRequest)
+ .addOnSuccessListener { result: BeginSignInResult ->
+ try {
+ startIntentSenderForResult(
+ result.pendingIntent.intentSender,
+ REQUEST_CODE_BEGIN_SIGN_IN,
+ null, /* fillInIntent= */
+ 0, /* flagsMask= */
+ 0, /* flagsValue= */
+ 0, /* extraFlags= */
+ null /* options= */
+ )
+ } catch (e: IntentSender.SendIntentException) {
+ Log.e(TAG, "Couldn't start One Tap UI in beginSignIn: " +
+ e.localizedMessage
+ )
+ val exception: GetCredentialException = GetCredentialUnknownException(
+ e.localizedMessage)
+ executor.execute { ->
+ callback.onError(exception)
+ }
+ }
+ }
+ .addOnFailureListener { e: Exception ->
+ // No saved credentials found. Launch the One Tap sign-up flow, or
+ // do nothing and continue presenting the signed-out UI.
+ Log.i(TAG, "Failure in begin sign in call")
+ if (e.localizedMessage != null) { Log.i(TAG, e.localizedMessage!!) }
+ var exception: GetCredentialException = GetCredentialUnknownException()
+ if (e is ApiException && e.statusCode in this.retryables) {
+ exception = GetCredentialInterruptedException(e.localizedMessage)
+ }
+ executor.execute { ->
+ callback.onError(
+ exception
+ )
+ }
+ }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -64,17 +120,68 @@
}
private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
- Log.i(TAG, "$uniqueRequestCode $resultCode $data")
- TODO("Not yet implemented")
+ if (uniqueRequestCode != REQUEST_CODE_BEGIN_SIGN_IN) {
+ Log.i(TAG, "returned request code does not match what was given")
+ return
+ }
+ if (resultCode != Activity.RESULT_OK) {
+ var exception: GetCredentialException = GetCredentialUnknownException()
+ if (resultCode == Activity.RESULT_CANCELED) {
+ exception = GetCredentialCanceledException()
+ }
+ this.executor.execute { -> this.callback.onError(exception) }
+ return
+ }
+ try {
+ val signInCredential = Identity.getSignInClient(activity as Activity)
+ .getSignInCredentialFromIntent(data)
+ Log.i(TAG, "Credential returned : " + signInCredential.googleIdToken + " , " +
+ signInCredential.id + ", " + signInCredential.password)
+ val response = convertResponseToCredentialManager(signInCredential)
+ Log.i(TAG, "Credential : " + response.credential.toString())
+ this.executor.execute { this.callback.onResult(response) }
+ } catch (e: ApiException) {
+ var exception: GetCredentialException = GetCredentialUnknownException()
+ if (e.statusCode == CommonStatusCodes.CANCELED) {
+ Log.i(TAG, "User cancelled the prompt!")
+ exception = GetCredentialCanceledException()
+ } else if (e.statusCode in this.retryables) {
+ exception = GetCredentialInterruptedException()
+ }
+ executor.execute { ->
+ callback.onError(
+ exception
+ )
+ }
+ return
+ }
}
- override fun convertRequestToPlayServices(request: GetCredentialRequest): BeginSignInRequest {
- TODO("Not yet implemented")
+ override fun convertRequestToPlayServices(request: GetCredentialRequest):
+ BeginSignInRequest {
+ return constructBeginSignInRequest(request)
}
override fun convertResponseToCredentialManager(response: SignInCredential):
GetCredentialResponse {
- TODO("Not yet implemented")
+ var cred: Credential? = null
+ if (response.password != null) {
+ cred = PasswordCredential(response.id, response.password!!)
+ } else if (response.googleIdToken != null) {
+ TODO(" Implement GoogleIdTokenVersion")
+ }
+ // TODO("Implement PublicKeyCredential Version")
+ else {
+ Log.i(TAG, "Credential returned but no google Id or password or passkey found")
+ }
+ if (cred == null) {
+ executor.execute { callback.onError(
+ GetCredentialUnknownException(
+ "cred should not be null")
+ ) }
+ return GetCredentialResponse(Credential("ERROR", Bundle.EMPTY))
+ }
+ return GetCredentialResponse(cred)
}
companion object {
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
index 2261081..be6ba81 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -16,14 +16,27 @@
package androidx.credentials.playservices.controllers.CreatePassword
+import android.annotation.SuppressLint
+import android.app.Activity
import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePasswordResponse
import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.exceptions.CreateCredentialCanceledException
import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialInterruptedException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.identity.SavePasswordRequest
+import com.google.android.gms.auth.api.identity.SavePasswordResult
+import com.google.android.gms.auth.api.identity.SignInPassword
+import com.google.android.gms.common.api.ApiException
import java.util.concurrent.Executor
/**
@@ -50,37 +63,97 @@
*/
private lateinit var executor: Executor
+ @SuppressLint("ClassVerificationFailure")
override fun invokePlayServices(
request: CreatePasswordRequest,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
executor: Executor
) {
- TODO("Not yet implemented")
+ this.callback = callback
+ this.executor = executor
+ val convertedRequest: SavePasswordRequest = this.convertRequestToPlayServices(request)
+ Identity.getCredentialSavingClient(activity)
+ .savePassword(convertedRequest)
+ .addOnSuccessListener { result: SavePasswordResult ->
+ try {
+ if (Build.VERSION.SDK_INT >= 24) {
+ startIntentSenderForResult(
+ result.pendingIntent.intentSender,
+ REQUEST_CODE_GIS_SAVE_PASSWORD,
+ null, /* fillInIntent= */
+ 0, /* flagsMask= */
+ 0, /* flagsValue= */
+ 0, /* extraFlags= */
+ null /* options= */
+ )
+ }
+ } catch (e: IntentSender.SendIntentException) {
+ Log.i(
+ TAG, "Failed to send pending intent for savePassword" +
+ " : " + e.message
+ )
+ val exception: CreateCredentialException = CreateCredentialUnknownException()
+ executor.execute { ->
+ callback.onError(
+ exception
+ )
+ }
+ }
+ }
+ .addOnFailureListener { e: Exception ->
+ Log.i(TAG, "CreatePassword failed with : " + e.message)
+ var exception: CreateCredentialException = CreateCredentialUnknownException()
+ if (e is ApiException && e.statusCode in this.retryables) {
+ exception = CreateCredentialInterruptedException()
+ }
+ executor.execute { ->
+ callback.onError(
+ exception
+ )
+ }
+ }
}
-
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
handleResponse(requestCode, resultCode, data)
}
private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
- Log.i(TAG, "$uniqueRequestCode $resultCode $data")
- TODO("Not yet implemented")
+ Log.i(TAG, "$data - the intent back - is un-used.")
+ if (uniqueRequestCode != REQUEST_CODE_GIS_SAVE_PASSWORD) {
+ return
+ }
+ if (resultCode != Activity.RESULT_OK) {
+ var exception: CreateCredentialException = CreateCredentialUnknownException()
+ if (resultCode == Activity.RESULT_CANCELED) {
+ exception = CreateCredentialCanceledException()
+ }
+ this.executor.execute { -> this.callback.onError(exception) }
+ return
+ }
+ Log.i(TAG, "Saving password succeeded")
+ val response: CreateCredentialResponse = convertResponseToCredentialManager(Unit)
+ this.executor.execute { -> this.callback.onResult(response) }
}
- override fun convertRequestToPlayServices(request: CreatePasswordRequest): SavePasswordRequest {
- TODO("Not yet implemented")
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public override fun convertRequestToPlayServices(request: CreatePasswordRequest):
+ SavePasswordRequest {
+ return SavePasswordRequest.builder().setSignInPassword(
+ SignInPassword(request.id, request.password)
+ ).build()
}
- override fun convertResponseToCredentialManager(response: Unit): CreateCredentialResponse {
- TODO("Not yet implemented")
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public override fun convertResponseToCredentialManager(response: Unit):
+ CreateCredentialResponse {
+ return CreatePasswordResponse()
}
companion object {
private val TAG = CredentialProviderCreatePasswordController::class.java.name
private const val REQUEST_CODE_GIS_SAVE_PASSWORD: Int = 1
// TODO("Ensure this works with the lifecycle")
-
/**
* This finds a past version of the
* [CredentialProviderCreatePasswordController] if it exists, otherwise
@@ -120,4 +193,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
index ccaf643..6843502 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
@@ -37,8 +37,7 @@
*/
@Suppress("deprecation")
abstract class CredentialProviderController<T1 : Any, T2 : Any, R2 : Any, R1 : Any,
- E1 : Any> : android.app
- .Fragment() {
+ E1 : Any> : android.app.Fragment() {
protected var retryables: Set<Int> = setOf(INTERNAL_ERROR,
NETWORK_ERROR)
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index 6bb5a52..3d7dde9 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -53,11 +53,3 @@
inceptionYear = "2022"
description = "Android Credentials Library"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/datastore/datastore-rxjava2/api/current.txt b/datastore/datastore-rxjava2/api/current.txt
index 433238b..dc394de 100644
--- a/datastore/datastore-rxjava2/api/current.txt
+++ b/datastore/datastore-rxjava2/api/current.txt
@@ -27,7 +27,7 @@
method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava2.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.Scheduler scheduler);
}
- public interface RxSharedPreferencesMigration<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
}
diff --git a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
index e662ed7..fb412f3 100644
--- a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
@@ -29,7 +29,7 @@
method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava2.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.Scheduler scheduler);
}
- public interface RxSharedPreferencesMigration<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
}
diff --git a/datastore/datastore-rxjava2/api/restricted_current.txt b/datastore/datastore-rxjava2/api/restricted_current.txt
index 433238b..dc394de 100644
--- a/datastore/datastore-rxjava2/api/restricted_current.txt
+++ b/datastore/datastore-rxjava2/api/restricted_current.txt
@@ -27,7 +27,7 @@
method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava2.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.Scheduler scheduler);
}
- public interface RxSharedPreferencesMigration<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
index a24bcfa..ae5de44c 100644
--- a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
@@ -24,6 +24,7 @@
import io.reactivex.Single
import kotlinx.coroutines.rx2.await
+@JvmDefaultWithCompatibility
/**
* Client implemented migration interface.
**/
diff --git a/datastore/datastore-rxjava3/api/current.txt b/datastore/datastore-rxjava3/api/current.txt
index 1033671..218d1e9 100644
--- a/datastore/datastore-rxjava3/api/current.txt
+++ b/datastore/datastore-rxjava3/api/current.txt
@@ -27,7 +27,7 @@
method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava3.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.rxjava3.core.Scheduler scheduler);
}
- public interface RxSharedPreferencesMigration<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
}
diff --git a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
index 05fa237..025e898 100644
--- a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
@@ -29,7 +29,7 @@
method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava3.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.rxjava3.core.Scheduler scheduler);
}
- public interface RxSharedPreferencesMigration<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
}
diff --git a/datastore/datastore-rxjava3/api/restricted_current.txt b/datastore/datastore-rxjava3/api/restricted_current.txt
index 1033671..218d1e9 100644
--- a/datastore/datastore-rxjava3/api/restricted_current.txt
+++ b/datastore/datastore-rxjava3/api/restricted_current.txt
@@ -27,7 +27,7 @@
method public static <T> kotlin.properties.ReadOnlyProperty<android.content.Context,androidx.datastore.rxjava3.RxDataStore<T>> rxDataStore(String fileName, androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional kotlin.jvm.functions.Function1<? super android.content.Context,? extends java.util.List<? extends androidx.datastore.core.DataMigration<T>>> produceMigrations, optional io.reactivex.rxjava3.core.Scheduler scheduler);
}
- public interface RxSharedPreferencesMigration<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface RxSharedPreferencesMigration<T> {
method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
index d53b48d..b159b92 100644
--- a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -24,6 +24,7 @@
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.rx3.await
+@JvmDefaultWithCompatibility
/**
* Client implemented migration interface.
**/
diff --git a/development/build_log_processor.sh b/development/build_log_processor.sh
index a9b931a..b55ad12 100755
--- a/development/build_log_processor.sh
+++ b/development/build_log_processor.sh
@@ -21,9 +21,6 @@
echo
echo "Executes <command> <arguments> and then runs build_log_simplifier.py against its output"
echo
- echo "-Pandroidx.summarizeStderr"
- echo " Run build_log_simplifier.py on failure to produce a summary of the build output"
- echo
echo "-Pandroidx.validateNoUnrecognizedMessages"
echo " Run build_log_simplifier.py --validate on success to confirm that the build generated no unrecognized messages"
exit 1
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index add01af..8cd2b88 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -792,3 +792,5 @@
xcframework successfully .*
# iOS benchmark invocation
/usr/bin/xcodebuild
+# > Configure project :internal-testutils-ktx
+WARNING:The option setting 'android\.r8\.maxWorkers=[0-9]+' is experimental\.
diff --git a/emoji2/emoji2-emojipicker/api/current.txt b/emoji2/emoji2-emojipicker/api/current.txt
index aab9e2c..f759566 100644
--- a/emoji2/emoji2-emojipicker/api/current.txt
+++ b/emoji2/emoji2-emojipicker/api/current.txt
@@ -8,9 +8,18 @@
method public float getEmojiGridRows();
method public void setEmojiGridColumns(int);
method public void setEmojiGridRows(float);
+ method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
property public final int emojiGridColumns;
property public final float emojiGridRows;
}
+ public final class EmojiViewItem {
+ ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+ method public String getEmoji();
+ method public java.util.List<java.lang.String> getVariants();
+ property public final String emoji;
+ property public final java.util.List<java.lang.String> variants;
+ }
+
}
diff --git a/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt b/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
index aab9e2c..f759566 100644
--- a/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
+++ b/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
@@ -8,9 +8,18 @@
method public float getEmojiGridRows();
method public void setEmojiGridColumns(int);
method public void setEmojiGridRows(float);
+ method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
property public final int emojiGridColumns;
property public final float emojiGridRows;
}
+ public final class EmojiViewItem {
+ ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+ method public String getEmoji();
+ method public java.util.List<java.lang.String> getVariants();
+ property public final String emoji;
+ property public final java.util.List<java.lang.String> variants;
+ }
+
}
diff --git a/emoji2/emoji2-emojipicker/api/restricted_current.txt b/emoji2/emoji2-emojipicker/api/restricted_current.txt
index aab9e2c..f759566 100644
--- a/emoji2/emoji2-emojipicker/api/restricted_current.txt
+++ b/emoji2/emoji2-emojipicker/api/restricted_current.txt
@@ -8,9 +8,18 @@
method public float getEmojiGridRows();
method public void setEmojiGridColumns(int);
method public void setEmojiGridRows(float);
+ method public void setOnEmojiPickedListener(androidx.core.util.Consumer<androidx.emoji2.emojipicker.EmojiViewItem>? onEmojiPickedListener);
property public final int emojiGridColumns;
property public final float emojiGridRows;
}
+ public final class EmojiViewItem {
+ ctor public EmojiViewItem(String emoji, java.util.List<java.lang.String> variants);
+ method public String getEmoji();
+ method public java.util.List<java.lang.String> getVariants();
+ property public final String emoji;
+ property public final java.util.List<java.lang.String> variants;
+ }
+
}
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index dd40d3a..bcc3091 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -33,6 +33,8 @@
implementation 'androidx.tracing:tracing-ktx:1.0.0'
implementation 'androidx.test.ext:junit-ktx:1.1.3'
+ androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
+ androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
index 8a8ebeb..baf6c45 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
+++ b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
@@ -17,11 +17,24 @@
package androidx.emoji2.emojipicker.samples
import android.os.Bundle
+import android.util.Log
import androidx.appcompat.app.AppCompatActivity
+import androidx.emoji2.emojipicker.EmojiPickerView
+import androidx.emoji2.emojipicker.EmojiViewItem
class MainActivity : AppCompatActivity() {
+ companion object {
+ internal const val LOG_TAG = "SAMPLE_APP"
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
+
+ val view = findViewById<EmojiPickerView>(R.id.emoji_picker)
+ view.setOnEmojiPickedListener { emoji: EmojiViewItem ->
+ Log.d(LOG_TAG, String.format("emoji: %s", emoji.emoji))
+ Log.d(LOG_TAG, String.format("variants: %s", emoji.variants.toString()))
+ }
}
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
index a63f725..b9b80a5 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
@@ -16,16 +16,34 @@
package androidx.emoji2.emojipicker
+import androidx.emoji2.emojipicker.R as EmojiPickerViewR
+import androidx.test.espresso.Espresso.onView
+import org.hamcrest.Description
import android.app.Activity
import android.content.Context
import android.os.Bundle
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.emoji2.emojipicker.test.R
+import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.longClick
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.BoundedMatcher
+import androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams
+import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -61,4 +79,112 @@
assertEquals(mEmojiPickerView.emojiGridColumns, 10)
}
}
+
+ @Test
+ fun testCustomEmojiPickerView_noVariant() {
+ mActivityTestRule.scenario.onActivity {
+ val targetView = findViewByEmoji(
+ it.findViewById(R.id.emojiPickerTest),
+ "\uD83D\uDE00"
+ )
+ // No variant indicator
+ assertEquals(
+ (targetView.parent as FrameLayout).findViewById<ImageView>(
+ EmojiPickerViewR.id.variant_availability_indicator
+ ).visibility,
+ GONE
+ )
+ // Not long-clickable
+ assertEquals(targetView.isLongClickable, false)
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 24)
+ fun testCustomEmojiPickerView_hasVariant() {
+ // 👃 has variants
+ val noseEmoji = "\uD83D\uDC43"
+ lateinit var bodyView: EmojiPickerBodyView
+ mActivityTestRule.scenario.onActivity {
+ bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
+ .findViewById(EmojiPickerViewR.id.emoji_picker_body)
+ }
+ onView(withId(EmojiPickerViewR.id.emoji_picker_body))
+ .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
+ val targetView = findViewByEmoji(bodyView, noseEmoji)
+ // Variant indicator visible
+ assertEquals(
+ (targetView.parent as FrameLayout).findViewById<ImageView>(
+ EmojiPickerViewR.id.variant_availability_indicator
+ ).visibility, VISIBLE
+ )
+ // Long-clickable
+ assertEquals(targetView.isLongClickable, true)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 24)
+ fun testStickyVariant_displayAndSaved() {
+ val noseEmoji = "\uD83D\uDC43"
+ val noseEmojiDark = "\uD83D\uDC43\uD83C\uDFFF"
+ lateinit var bodyView: EmojiPickerBodyView
+ mActivityTestRule.scenario.onActivity {
+ bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
+ .findViewById(EmojiPickerViewR.id.emoji_picker_body)
+ }
+ // Scroll to the nose emoji, long click then select nose in dark skin tone
+ onView(withId(EmojiPickerViewR.id.emoji_picker_body))
+ .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
+ onView(createEmojiViewMatcher(noseEmoji)).perform(longClick())
+ onView(createEmojiViewMatcher(noseEmojiDark))
+ .inRoot(hasWindowLayoutParams())
+ .perform(click())
+ assertNotNull(findViewByEmoji(bodyView, noseEmojiDark))
+ // Switch back to clear saved preference
+ onView(createEmojiViewMatcher(noseEmojiDark)).perform(longClick())
+ onView(createEmojiViewMatcher(noseEmoji))
+ .inRoot(hasWindowLayoutParams())
+ .perform(click())
+ assertNotNull(findViewByEmoji(bodyView, noseEmoji))
+ }
+
+ private fun findViewByEmoji(root: View, emoji: String) =
+ mutableListOf<View>().apply {
+ findViewsById(
+ root,
+ EmojiPickerViewR.id.emoji_view, this
+ )
+ }.first { (it as EmojiView).emoji == emoji }
+
+ private fun findViewsById(root: View, id: Int, output: MutableList<View>) {
+ if (root !is ViewGroup) {
+ return
+ }
+ for (i in 0 until root.childCount) {
+ root.getChildAt(i).apply {
+ if (this.id == id) {
+ output.add(this)
+ }
+ }.also {
+ findViewsById(it, id, output)
+ }
+ }
+ }
+
+ private fun createEmojiViewHolderMatcher(emoji: String) =
+ object :
+ BoundedMatcher<RecyclerView.ViewHolder, EmojiViewHolder>(EmojiViewHolder::class.java) {
+ override fun describeTo(description: Description) {}
+ override fun matchesSafely(item: EmojiViewHolder) =
+ (item.itemView as FrameLayout)
+ .findViewById<EmojiView>(EmojiPickerViewR.id.emoji_view)
+ .emoji == emoji
+ }
+
+ private fun createEmojiViewMatcher(emoji: String) =
+ object :
+ BoundedMatcher<View, EmojiView>(EmojiView::class.java) {
+ override fun describeTo(description: Description) {}
+ override fun matchesSafely(item: EmojiView) = item.emoji == emoji
+ }
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 3a4e7ed..57136d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -33,7 +33,7 @@
* @property categorizedEmojiData: a list that holds bundled emoji separated by category, filtered
* by renderability check. This is the data source for EmojiPickerView.
*
- * @property emojiVariantsLookup: a map of emoji variants in bundled emoji, keyed by the primary
+ * @property emojiVariantsLookup: a map of emoji variants in bundled emoji, keyed by the base
* emoji. This allows faster variants lookup.
*/
internal object BundledEmojiListLoader {
@@ -64,7 +64,7 @@
emojiVariantsLookup ?: getCategorizedEmojiData()
.flatMap { it.emojiDataList }
.filter { it.variants.isNotEmpty() }
- .associate { it.primary to it.variants }
+ .associate { it.emoji to it.variants }
.also { emojiVariantsLookup = it }
private suspend fun loadEmojiAsync(
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 9679844..8356d6c 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -22,24 +22,24 @@
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
-import androidx.annotation.IntRange
import androidx.annotation.UiThread
-import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.util.Consumer
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.tracing.Trace
+import androidx.emoji2.emojipicker.EmojiPickerConstants.RECENT_CATEGORY_INDEX
/** RecyclerView adapter for emoji body. */
internal class EmojiPickerBodyAdapter(
- context: Context,
+ private val context: Context,
private val emojiGridColumns: Int,
private val emojiGridRows: Float,
- private val categoryNames: Array<String>
+ private val categoryNames: Array<String>,
+ private val stickyVariantProvider: StickyVariantProvider,
+ private val onEmojiPickedListener: Consumer<EmojiViewItem>?
) : Adapter<ViewHolder>() {
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
- private val context = context
-
private var flattenSource: ItemViewDataFlatList
init {
@@ -58,47 +58,63 @@
Trace.beginSection("EmojiPickerBodyAdapter.onCreateViewHolder")
return try {
val view: View
- if (viewType == CategorySeparatorViewData.TYPE) {
- view = layoutInflater.inflate(
- R.layout.category_text_view,
- parent,
- /* attachToRoot= */ false
- )
- view.layoutParams =
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
- } else if (viewType == EmptyCategoryViewData.TYPE) {
- view = layoutInflater.inflate(
- R.layout.emoji_picker_empty_category_text_view,
- parent,
- /* attachToRoot= */ false
- )
- view.layoutParams =
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
- view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
- } else if (viewType == EmojiViewData.TYPE) {
- return EmojiViewHolder(
- parent,
- layoutInflater,
- getParentWidth(parent) / emojiGridColumns,
- (parent.measuredHeight / emojiGridRows).toInt(),
- )
- } else if (viewType == DummyViewData.TYPE) {
- view = View(context)
- view.layoutParams = LayoutParams(
- getParentWidth(parent) / emojiGridColumns,
- (parent.measuredHeight / emojiGridRows).toInt()
- )
- } else {
- Log.e(
- "EmojiPickerBodyAdapter",
- "EmojiPickerBodyAdapter gets unsupported view type."
- )
- view = View(context)
- view.layoutParams =
- LayoutParams(
+ when (viewType) {
+ CategorySeparatorViewData.TYPE -> {
+ view = layoutInflater.inflate(
+ R.layout.category_text_view,
+ parent,
+ /* attachToRoot = */ false
+ )
+ view.layoutParams =
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+ }
+
+ EmptyCategoryViewData.TYPE -> {
+ view = layoutInflater.inflate(
+ R.layout.emoji_picker_empty_category_text_view,
+ parent,
+ /* attachToRoot = */ false
+ )
+ view.layoutParams =
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+ view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
+ }
+
+ EmojiViewData.TYPE -> {
+ return EmojiViewHolder(
+ parent,
+ layoutInflater,
+ getParentWidth(parent) / emojiGridColumns,
+ (parent.measuredHeight / emojiGridRows).toInt(),
+ stickyVariantProvider,
+ onEmojiPickedListener,
+ onEmojiPickedFromPopupListener = { emoji ->
+ (flattenSource[bindingAdapterPosition] as EmojiViewData).primary = emoji
+ notifyItemChanged(bindingAdapterPosition)
+ }
+ )
+ }
+
+ DummyViewData.TYPE -> {
+ view = View(context)
+ view.layoutParams = LayoutParams(
getParentWidth(parent) / emojiGridColumns,
(parent.measuredHeight / emojiGridRows).toInt()
)
+ }
+
+ else -> {
+ Log.e(
+ "EmojiPickerBodyAdapter",
+ "EmojiPickerBodyAdapter gets unsupported view type."
+ )
+ view = View(context)
+ view.layoutParams =
+ LayoutParams(
+ getParentWidth(parent) / emojiGridColumns,
+ (parent.measuredHeight / emojiGridRows).toInt()
+ )
+ }
}
object : ViewHolder(view) {}
} finally {
@@ -116,13 +132,19 @@
if (categoryName.isEmpty()) {
categoryName = categoryNames[categoryIndex]
}
+
// Show category label.
val categoryLabel = view.findViewById<AppCompatTextView>(R.id.category_name)
- if (categoryName.isEmpty()) {
- categoryLabel.visibility = View.GONE
- } else {
- categoryLabel.text = categoryName
+ if (categoryIndex == RECENT_CATEGORY_INDEX) {
+ categoryLabel.text = context.getString(R.string.emoji_category_recent)
categoryLabel.visibility = View.VISIBLE
+ } else {
+ if (categoryName.isEmpty()) {
+ categoryLabel.visibility = View.GONE
+ } else {
+ categoryLabel.text = categoryName
+ categoryLabel.visibility = View.VISIBLE
+ }
}
} else if (viewType == EmptyCategoryViewData.TYPE) {
// Show empty category description.
@@ -131,9 +153,9 @@
val item = flattenSource[position] as EmptyCategoryViewData
var content = item.description
if (content.isEmpty()) {
- val categoryIndex: Int = getCategoryIndex(position)
+ val categoryIndex = flattenSource.getCategoryIndex(position)
content = context.getString(
- if (categoryIndex == EmojiPickerConstants.RECENT_CATEGORY_INDEX)
+ if (categoryIndex == RECENT_CATEGORY_INDEX)
R.string.emoji_empty_recent_category
else R.string.emoji_empty_non_recent_category
)
@@ -159,16 +181,6 @@
return flattenSource[position].type
}
- @IntRange(from = 0)
- fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
- return getFlattenSource().getCategoryIndex(position)
- }
-
- @VisibleForTesting
- fun getFlattenSource(): ItemViewDataFlatList {
- return flattenSource
- }
-
private fun getParentWidth(parent: ViewGroup): Int {
return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 178c0cb..2abfc99 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -21,6 +21,7 @@
import android.os.Trace
import android.util.AttributeSet
import android.widget.FrameLayout
+import androidx.core.util.Consumer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
@@ -60,8 +61,11 @@
field = if (value > 0) value else EmojiPickerConstants.DEFAULT_BODY_COLUMNS
}
+ private val stickyVariantProvider = StickyVariantProvider(context)
+
private lateinit var headerView: RecyclerView
private lateinit var bodyView: RecyclerView
+ private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null
init {
val typedArray: TypedArray =
@@ -84,25 +88,36 @@
}
}
- private fun getEmojiPickerBodyAdapter(
+ private fun createEmojiPickerBodyAdapter(
context: Context,
emojiGridColumns: Int,
emojiGridRows: Float,
- categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>
+ categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>,
+ onEmojiPickedListener: Consumer<EmojiViewItem>?
): EmojiPickerBodyAdapter {
val categoryNames = mutableListOf<String>()
val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
+ // add recent category as the first row
+ categoryNames.add(resources.getString(R.string.emoji_category_recent))
+ categorizedEmojis.add(mutableListOf())
+
for (i in categorizedEmojiData.indices) {
categoryNames.add(categorizedEmojiData[i].categoryName)
categorizedEmojis.add(
- categorizedEmojiData[i].emojiDataList.toMutableList()
+ categorizedEmojiData[i].emojiDataList.map {
+ stickyVariantProvider.stickyVariantMap[it.emoji]?.let { stickyVariant ->
+ EmojiViewItem(stickyVariant, it.variants)
+ } ?: it
+ }.toMutableList()
)
}
val adapter = EmojiPickerBodyAdapter(
context,
emojiGridColumns,
emojiGridRows,
- categoryNames.toTypedArray()
+ categoryNames.toTypedArray(),
+ stickyVariantProvider,
+ onEmojiPickedListener
)
adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
@@ -121,7 +136,7 @@
EmojiViewData(
categoryIndex,
idInCategory,
- eachEmoji.primary,
+ eachEmoji.emoji,
eachEmoji.variants.toTypedArray()
)
)
@@ -144,15 +159,28 @@
LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
- /* reverseLayout= */ false
+ /* reverseLayout = */ false
)
headerView.adapter = EmojiPickerHeaderAdapter(context)
// set bodyView
bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
- bodyView.adapter = getEmojiPickerBodyAdapter(
- context, emojiGridColumns, emojiGridRows, categorizedEmojiData
- )
+ bodyView.adapter =
+ createEmojiPickerBodyAdapter(
+ context,
+ emojiGridColumns,
+ emojiGridRows,
+ categorizedEmojiData,
+ onEmojiPickedListener
+ )
}
-}
\ No newline at end of file
+
+ /**
+ * This function is used to set the custom behavior after clicking on an emoji icon. Clients
+ * could specify their own behavior inside this function.
+ */
+ fun setOnEmojiPickedListener(onEmojiPickedListener: Consumer<EmojiViewItem>?) {
+ this.onEmojiPickedListener = onEmojiPickedListener
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
index bba8664..1e12a47 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
@@ -80,6 +80,7 @@
if (value != null) {
post {
drawEmoji(value)
+ contentDescription = value
invalidate()
}
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
index aace300..3c34b4d 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
@@ -30,8 +30,9 @@
/** The id of this emoji view in the category, usually is the position of the emoji. */
private val idInCategory: Int
- /** Primary key which is used for labeling and for PRESS action. */
- val primary: String
+ /** Primary key which is used for labeling and for PRESS action. This value could be updated
+ * to one of its variants. */
+ var primary: String
/** Secondary keys which are used for LONG_PRESS action. */
val secondaries: Array<String>
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
index 4dc5cd7..cbe6e27 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -16,32 +16,134 @@
package androidx.emoji2.emojipicker
+import android.view.Gravity
import android.view.LayoutInflater
+import android.view.View.GONE
+import android.view.View.OnClickListener
+import android.view.View.OnLongClickListener
+import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.GridLayout
+import android.widget.ImageView
+import android.widget.PopupWindow
+import androidx.core.util.Consumer
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import kotlin.math.roundToInt
/** A [ViewHolder] containing an emoji view and emoji data. */
internal class EmojiViewHolder(
parent: ViewGroup,
layoutInflater: LayoutInflater,
width: Int,
- height: Int
+ height: Int,
+ stickyVariantProvider: StickyVariantProvider,
+ onEmojiPickedListener: Consumer<EmojiViewItem>?,
+ onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
) : ViewHolder(
layoutInflater
- .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot= */false)
+ .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot = */false)
) {
+ private val onEmojiClickListener: OnClickListener = OnClickListener {
+ // TODO(scduan): Add other on click events (e.g, add to recent)
+ onEmojiPickedListener?.accept(emojiViewItem)
+ }
+
+ private val onEmojiLongClickListener: OnLongClickListener = OnLongClickListener {
+ val variants = emojiViewItem.variants
+ val popupView = layoutInflater
+ .inflate(R.layout.variant_popup, null, false)
+ .findViewById<GridLayout>(R.id.variant_popup)
+ .apply {
+ // Show 6 emojis in one row at most
+ this.columnCount = minOf(6, variants.size)
+ this.rowCount =
+ variants.size / this.columnCount +
+ if (variants.size % this.columnCount == 0) 0 else 1
+ this.orientation = GridLayout.HORIZONTAL
+ }
+ val popupWindow = showPopupWindow(emojiView, popupView)
+ for (v in variants) {
+ // Add variant emoji view to the popup view
+ layoutInflater
+ .inflate(R.layout.emoji_view_holder, null, false).apply {
+ this as FrameLayout
+ (getChildAt(0) as EmojiView).emoji = v
+ setOnClickListener {
+ onEmojiPickedFromPopupListener(this@EmojiViewHolder, v)
+ onEmojiClickListener.onClick(it)
+ // variants[0] is always the base (i.e., primary) emoji
+ stickyVariantProvider.update(variants[0], v)
+ popupWindow.dismiss()
+ // Hover on the base emoji after popup dismissed
+ emojiView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+ }
+ }.also {
+ popupView.addView(it)
+ it.layoutParams.width = emojiView.measuredWidth
+ it.layoutParams.height = emojiView.measuredHeight
+ }
+ }
+ popupView.post {
+ // Hover on the first emoji in the popup
+ (popupView.getChildAt(0) as FrameLayout).getChildAt(0)
+ .sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+ }
+ true
+ }
+
private val emojiView: EmojiView
+ private val indicator: ImageView
+ private lateinit var emojiViewItem: EmojiViewItem
init {
itemView.layoutParams = LayoutParams(width, height)
emojiView = itemView.findViewById(R.id.emoji_view)
emojiView.isClickable = true
+ emojiView.setOnClickListener(onEmojiClickListener)
+ indicator = itemView.findViewById(R.id.variant_availability_indicator)
}
fun bindEmoji(
- emojiViewItem: EmojiViewItem
+ emojiViewItem: EmojiViewItem,
) {
- emojiView.emoji = emojiViewItem.primary
+ emojiView.emoji = emojiViewItem.emoji
+ this.emojiViewItem = emojiViewItem
+
+ if (emojiViewItem.variants.isNotEmpty()) {
+ indicator.visibility = VISIBLE
+ emojiView.setOnLongClickListener(onEmojiLongClickListener)
+ emojiView.isLongClickable = true
+ } else {
+ indicator.visibility = GONE
+ emojiView.setOnLongClickListener(null)
+ emojiView.isLongClickable = false
+ }
+ }
+
+ private fun showPopupWindow(
+ parent: EmojiView,
+ popupView: GridLayout
+ ): PopupWindow {
+ val location = IntArray(2)
+ parent.getLocationInWindow(location)
+ // Make the popup view center align with the target emoji view.
+ val x =
+ location[0] + parent.width / 2f - popupView.columnCount * parent.width / 2f
+ val y = location[1] - popupView.rowCount * parent.height
+ return PopupWindow(
+ popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, false
+ ).also {
+ it.isOutsideTouchable = true
+ it.isTouchable = true
+ it.showAtLocation(
+ parent,
+ Gravity.NO_GRAVITY,
+ x.roundToInt(),
+ y
+ )
+ }
}
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
index 2f79955..e6121f1 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
@@ -16,4 +16,10 @@
package androidx.emoji2.emojipicker
-internal class EmojiViewItem(val primary: String, val variants: List<String>)
\ No newline at end of file
+/**
+ * [EmojiViewItem] is a class holding the displayed emoji and its emoji variants
+ *
+ * @param emoji Used to represent the displayed emoji of the [EmojiViewItem].
+ * @param variants Used to represent the corresponding emoji variants of this base emoji.
+ */
+class EmojiViewItem(val emoji: String, val variants: List<String>)
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
new file mode 100644
index 0000000..7c45cf6
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 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.emoji2.emojipicker
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+
+/**
+ * A class that handles user's emoji variant selection using SharedPreferences.
+ */
+internal class StickyVariantProvider(context: Context) {
+ companion object {
+ const val PREFERENCES_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
+ const val STICKY_VARIANT_PROVIDER_KEY = "pref_key_sticky_variant"
+ const val KEY_VALUE_DELIMITER = "="
+ const val ENTRY_DELIMITER = "|"
+ }
+
+ private val sharedPreferences =
+ context.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE)
+
+ internal val stickyVariantMap: Map<String, String> by lazy {
+ sharedPreferences.getString(STICKY_VARIANT_PROVIDER_KEY, null)?.split(ENTRY_DELIMITER)
+ ?.associate { entry ->
+ entry.split(KEY_VALUE_DELIMITER, limit = 2).takeIf { it.size == 2 }
+ ?.let { it[0] to it[1] } ?: ("" to "")
+ } ?: mapOf()
+ }
+
+ internal fun update(baseEmoji: String, variantClicked: String) {
+ stickyVariantMap.toMutableMap().apply {
+ if (baseEmoji == variantClicked) {
+ this.remove(baseEmoji)
+ } else {
+ this[baseEmoji] = variantClicked
+ }
+ sharedPreferences.edit()
+ .putString(
+ STICKY_VARIANT_PROVIDER_KEY,
+ entries.joinToString(ENTRY_DELIMITER)
+ ).commit()
+ }
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
index 7b1bdaf..b724ba9 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
@@ -83,7 +83,7 @@
targetFile.bufferedWriter()
.use { out ->
for (emoji in data) {
- out.write(emoji.primary)
+ out.write(emoji.emoji)
emoji.variants.forEach { out.write(",$it") }
out.newLine()
}
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
new file mode 100644
index 0000000..1715a56
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2022 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:tint="?attr/colorControlNormal">
+ <corners android:radius="@dimen/emoji_picker_popup_view_holder_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
new file mode 100644
index 0000000..e27fe2e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -0,0 +1,26 @@
+<!--
+ Copyright 2022 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M2,22h20V2L2,22z"/>
+</vector>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
index 0fa8c9c..76fe2b9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -15,7 +15,6 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="0dp"
android:layout_height="0dp">
@@ -28,4 +27,11 @@
android:importantForAccessibility="yes"
android:textSize="30dp"
tools:ignore="SpUsage" />
+ <ImageView
+ android:id="@+id/variant_availability_indicator"
+ android:visibility="gone"
+ android:src="@drawable/variant_availability_indicator"
+ android:layout_width="5dp"
+ android:layout_height="5dp"
+ android:layout_gravity="bottom|end"/>
</FrameLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
new file mode 100644
index 0000000..d61780e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2022 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.
+ -->
+<GridLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/variant_popup"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ style="@style/EmojiPickerStylePopupViewHolder">
+</GridLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index cc79c54..207125d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -24,4 +24,11 @@
<dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
<dimen name="emoji_picker_header_height">50dp</dimen>
<dimen name="emoji_picker_header_padding">8dp</dimen>
+ <dimen name="variant_availability_indicator_height">5dp</dimen>
+ <dimen name="variant_availability_indicator_width">5dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_padding_vertical">3dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_padding_start">3dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_padding_end">5dp</dimen>
+ <dimen name="emoji_picker_popup_view_elevation">2dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_corner_radius">10dp</dimen>
</resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
index 472702f..c883441 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -15,6 +15,8 @@
-->
<resources>
+ <!-- Emoji recent sub category. -->
+ <string name="emoji_category_recent">RECENTLY USED</string>
<!-- Emoji keyboard's smileys and emotions sub category. -->
<string name="emoji_category_emotions">SMILEYS AND EMOTIONS</string>
<!-- Emoji keyboard's people sub category. -->
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
index 0e6d24e..d715e58 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -31,4 +31,13 @@
<style name="EmojiPickerStyleCategoryLabelText">
<item name="android:textColor">?attr/EmojiPickerColorCategoryLabelText</item>
</style>
+
+ <style name="EmojiPickerStylePopupViewHolder">
+ <item name="android:padding">@dimen/emoji_picker_popup_view_holder_padding_vertical</item>
+ <item name="android:paddingStart">@dimen/emoji_picker_popup_view_holder_padding_start</item>
+ <item name="android:paddingEnd">@dimen/emoji_picker_popup_view_holder_padding_end</item>
+ <item name="android:background">@drawable/popup_view_rounded_background</item>
+ <item name="android:elevation">@dimen/emoji_picker_popup_view_elevation</item>
+ </style>
+
</resources>
diff --git a/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt b/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt
index 59134a1..d986e5e 100644
--- a/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt
+++ b/fragment/fragment-testing/src/androidTest/java/androidx/fragment/app/testing/FragmentScenarioDialogFragmentTest.kt
@@ -27,6 +27,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.hamcrest.CoreMatchers.not
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,6 +37,8 @@
@RunWith(AndroidJUnit4::class)
@LargeTest
class FragmentScenarioDialogFragmentTest {
+
+ @Ignore // b/259726188
@Test
fun launchFragment() {
with(launchFragment<SimpleDialogFragment>()) {
@@ -48,6 +51,7 @@
}
}
+ @Ignore // b/259727355
@Test
fun launchFragmentInContainer() {
with(launchFragmentInContainer<SimpleDialogFragment>()) {
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index ff192ef..0fe45ab 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -254,7 +254,7 @@
method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
- @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyListScope {
+ @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
field public static final androidx.glance.appwidget.lazy.LazyListScope.Companion Companion;
@@ -276,7 +276,7 @@
method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
- @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+ @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyVerticalGridScope {
method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index cd1190b..68e9f6c 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -268,7 +268,7 @@
method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
- @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyListScope {
+ @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
field public static final androidx.glance.appwidget.lazy.LazyListScope.Companion Companion;
@@ -290,7 +290,7 @@
method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
- @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+ @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyVerticalGridScope {
method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index ff192ef..0fe45ab 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -254,7 +254,7 @@
method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
- @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyListScope {
+ @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
field public static final androidx.glance.appwidget.lazy.LazyListScope.Companion Companion;
@@ -276,7 +276,7 @@
method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
- @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+ @androidx.glance.appwidget.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyVerticalGridScope {
method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
diff --git a/glance/glance-appwidget/integration-tests/template-demos/build.gradle b/glance/glance-appwidget/integration-tests/template-demos/build.gradle
index 6f5933f..ec43ef6 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/build.gradle
+++ b/glance/glance-appwidget/integration-tests/template-demos/build.gradle
@@ -27,6 +27,8 @@
implementation(libs.kotlinStdlib)
implementation(project(":glance:glance"))
implementation(project(":glance:glance-appwidget"))
+ implementation(project(":glance:glance-material"))
+ implementation(project(":glance:glance-material3"))
implementation("androidx.activity:activity:1.4.0")
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.compose.material:material:1.1.0-beta02")
@@ -36,6 +38,7 @@
implementation("androidx.compose.material:material:1.1.0-beta02")
implementation("androidx.datastore:datastore-preferences-core:1.0.0")
implementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+ implementation "androidx.compose.material3:material3:1.0.0"
}
android {
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 189229d..0036785 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -91,7 +91,7 @@
</receiver>
<receiver
- android:name="androidx.glance.appwidget.template.demos.FullHeaderListReceiver"
+ android:name="androidx.glance.appwidget.template.demos.FullHeaderThemedListReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/list_style_with_header">
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
index 464d336..fd1b18a 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
@@ -64,11 +64,11 @@
/**
* List demo with list items in full details and list header without action button using data and
- * list template from [BaseListDemoWidget].
+ * list template from [BaseListDemoWidget] with custom theme.
*/
-class FullHeaderListDemoWidget : BaseListDemoWidget() {
+class FullHeaderListThemedDemoWidget : BaseListDemoWidget() {
@Composable
- override fun TemplateContent() = ListTemplateContent(ListStyle.Full, true)
+ override fun TemplateContent() = ListTemplateContent(ListStyle.Full, true, true)
}
/**
@@ -93,8 +93,8 @@
override val glanceAppWidget: GlanceAppWidget = FullHeaderActionListDemoWidget()
}
-class FullHeaderListReceiver : GlanceAppWidgetReceiver() {
- override val glanceAppWidget: GlanceAppWidget = FullHeaderListDemoWidget()
+class FullHeaderThemedListReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = FullHeaderListThemedDemoWidget()
}
class NoHeaderListReceiver : GlanceAppWidgetReceiver() {
@@ -124,14 +124,16 @@
* @param listStyle styling the list by [ListStyle] based data details
* @param initialNumItems initial number of list items to generate in the demo
* @param showHeader whether to show list header as a whole
+ * @param customTheme whether to override Glance system theme with custom theme
*/
@Composable
internal fun ListTemplateContent(
listStyle: ListStyle,
showHeader: Boolean = false,
+ customTheme: Boolean = false,
initialNumItems: Int = MAX_ITEMS,
) {
- GlanceTheme {
+ GlanceTheme(if (customTheme) PalmLeafScheme.colors else GlanceTheme.colors) {
val state = currentState<Preferences>()
val content = mutableListOf<ListTemplateItem>()
for (i in 1..(state[CountKey] ?: initialNumItems)) {
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt
new file mode 100644
index 0000000..b5a4029
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2022 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.glance.appwidget.template.demos
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.Color
+import androidx.glance.material3.ColorProviders
+
+/**
+ * Custom Color theme for the Palm Leaf themed demo templates
+ */
+object PalmLeafScheme {
+ val md_theme_light_primary = Color(0xFF026E00)
+ val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+ val md_theme_light_primaryContainer = Color(0xFFD7E8CD)
+ val md_theme_light_onPrimaryContainer = Color(0xFF002200)
+ val md_theme_light_secondary = Color(0xFFA900A9)
+ val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+ val md_theme_light_secondaryContainer = Color(0xFFFFD7F5)
+ val md_theme_light_onSecondaryContainer = Color(0xFF380038)
+ val md_theme_light_tertiary = Color(0xFF006A6A)
+ val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+ val md_theme_light_tertiaryContainer = Color(0xFF00FBFB)
+ val md_theme_light_onTertiaryContainer = Color(0xFF002020)
+ val md_theme_light_error = Color(0xFFBA1A1A)
+ val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+ val md_theme_light_onError = Color(0xFFFFFFFF)
+ val md_theme_light_onErrorContainer = Color(0xFF410002)
+ val md_theme_light_background = Color(0xFFFFFBFF)
+ val md_theme_light_onBackground = Color(0xFF1E1C00)
+ val md_theme_light_surface = Color(0xFFFFFBFF)
+ val md_theme_light_onSurface = Color(0xFF1E1C00)
+ val md_theme_light_surfaceVariant = Color(0xFFDFE4D7)
+ val md_theme_light_onSurfaceVariant = Color(0xFF43483F)
+ val md_theme_light_outline = Color(0xFF73796E)
+ val md_theme_light_inverseOnSurface = Color(0xFFFFF565)
+ val md_theme_light_inverseSurface = Color(0xFF353200)
+ val md_theme_light_inversePrimary = Color(0xFF02E600)
+ val md_theme_light_shadow = Color(0xFF000000)
+ val md_theme_light_surfaceTint = Color(0xFF026E00)
+
+ val md_theme_dark_primary = Color(0xFF02E600)
+ val md_theme_dark_onPrimary = Color(0xFF013A00)
+ val md_theme_dark_primaryContainer = Color(0xFFD7E8CD)
+ val md_theme_dark_onPrimaryContainer = Color(0xFF77FF61)
+ val md_theme_dark_secondary = Color(0xFFFFABF3)
+ val md_theme_dark_onSecondary = Color(0xFF5B005B)
+ val md_theme_dark_secondaryContainer = Color(0xFF810081)
+ val md_theme_dark_onSecondaryContainer = Color(0xFFFFD7F5)
+ val md_theme_dark_tertiary = Color(0xFF00DDDD)
+ val md_theme_dark_onTertiary = Color(0xFF003737)
+ val md_theme_dark_tertiaryContainer = Color(0xFF004F4F)
+ val md_theme_dark_onTertiaryContainer = Color(0xFF00FBFB)
+ val md_theme_dark_error = Color(0xFFFFB4AB)
+ val md_theme_dark_errorContainer = Color(0xFF93000A)
+ val md_theme_dark_onError = Color(0xFF690005)
+ val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+ val md_theme_dark_background = Color(0xFF1E1C00)
+ val md_theme_dark_onBackground = Color(0xFFF2E720)
+ val md_theme_dark_surface = Color(0xFF1E1C00)
+ val md_theme_dark_onSurface = Color(0xFFF2E720)
+ val md_theme_dark_surfaceVariant = Color(0xFF43483F)
+ val md_theme_dark_onSurfaceVariant = Color(0xFFC3C8BC)
+ val md_theme_dark_outline = Color(0xFF8D9387)
+ val md_theme_dark_inverseOnSurface = Color(0xFF1E1C00)
+ val md_theme_dark_inverseSurface = Color(0xFFF2E720)
+ val md_theme_dark_inversePrimary = Color(0xFF026E00)
+ val md_theme_dark_shadow = Color(0xFF000000)
+ val md_theme_dark_surfaceTint = Color(0xFF02E600)
+
+ val seed = Color(0xFF00FF00)
+
+ val LightColors = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ onError = md_theme_light_onError,
+ errorContainer = md_theme_light_errorContainer,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseSurface = md_theme_light_inverseSurface,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ )
+
+ val DarkColors = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ onError = md_theme_dark_onError,
+ errorContainer = md_theme_dark_errorContainer,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ )
+
+ val colors = ColorProviders(
+ light = LightColors,
+ dark = DarkColors
+ )
+}
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
index 4ee5288..0c49cae 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
@@ -27,7 +27,7 @@
<!-- Styles of the List Template widgets -->
<string name="list_style_with_header_action">List Template with header and action demo</string>
- <string name="list_style_with_header">List Template with header demo</string>
+ <string name="list_style_with_header">List Template with header and custom theme demo</string>
<string name="list_style_no_header">List Template with no header demo</string>
<string name="list_style_brief">List Template with no header demo in brief info</string>
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 8e5d0a2..4b49d42 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -102,6 +102,7 @@
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -755,6 +756,7 @@
}
}
+ @Ignore // b/259718271
@Test
fun compoundButtonAction() {
val checkbox = "checkbox"
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
index ff7adf8..d68d7e7 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
@@ -124,6 +124,7 @@
@LazyScopeMarker
interface LazyItemScope
+@JvmDefaultWithCompatibility
/**
* Receiver scope which is used by [LazyColumn].
*/
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
index 8ab2849..cf37b7e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
@@ -116,6 +116,7 @@
)
}
+@JvmDefaultWithCompatibility
/**
* Receiver scope which is used by [LazyColumn].
*/
diff --git a/glance/glance-wear-tiles/api/current.txt b/glance/glance-wear-tiles/api/current.txt
index ee79bb24..3a8208e 100644
--- a/glance/glance-wear-tiles/api/current.txt
+++ b/glance/glance-wear-tiles/api/current.txt
@@ -110,7 +110,7 @@
method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.glance.GlanceModifier modifier, optional float anchorDegrees, optional int anchorType, optional int radialAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.CurvedScope,kotlin.Unit> content);
}
- @androidx.glance.wear.tiles.curved.CurvedScopeMarker public interface CurvedScope {
+ @androidx.glance.wear.tiles.curved.CurvedScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface CurvedScope {
method public void curvedComposable(optional boolean rotateContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public void curvedLine(androidx.glance.unit.ColorProvider color, optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
method public void curvedSpacer(optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
@@ -132,7 +132,7 @@
property public final androidx.glance.text.FontWeight? fontWeight;
}
- @androidx.compose.runtime.Stable public interface GlanceCurvedModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceCurvedModifier {
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
@@ -148,7 +148,7 @@
method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? super R,? extends R> operation);
}
- public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
diff --git a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
index d6febbf..39c1be1 100644
--- a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
@@ -122,7 +122,7 @@
method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.glance.GlanceModifier modifier, optional float anchorDegrees, optional int anchorType, optional int radialAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.CurvedScope,kotlin.Unit> content);
}
- @androidx.glance.wear.tiles.curved.CurvedScopeMarker public interface CurvedScope {
+ @androidx.glance.wear.tiles.curved.CurvedScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface CurvedScope {
method public void curvedComposable(optional boolean rotateContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public void curvedLine(androidx.glance.unit.ColorProvider color, optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
method public void curvedSpacer(optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
@@ -144,7 +144,7 @@
property public final androidx.glance.text.FontWeight? fontWeight;
}
- @androidx.compose.runtime.Stable public interface GlanceCurvedModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceCurvedModifier {
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
@@ -160,7 +160,7 @@
method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? super R,? extends R> operation);
}
- public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
diff --git a/glance/glance-wear-tiles/api/restricted_current.txt b/glance/glance-wear-tiles/api/restricted_current.txt
index ee79bb24..3a8208e 100644
--- a/glance/glance-wear-tiles/api/restricted_current.txt
+++ b/glance/glance-wear-tiles/api/restricted_current.txt
@@ -110,7 +110,7 @@
method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.glance.GlanceModifier modifier, optional float anchorDegrees, optional int anchorType, optional int radialAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.CurvedScope,kotlin.Unit> content);
}
- @androidx.glance.wear.tiles.curved.CurvedScopeMarker public interface CurvedScope {
+ @androidx.glance.wear.tiles.curved.CurvedScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface CurvedScope {
method public void curvedComposable(optional boolean rotateContent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public void curvedLine(androidx.glance.unit.ColorProvider color, optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
method public void curvedSpacer(optional androidx.glance.wear.tiles.curved.GlanceCurvedModifier curvedModifier);
@@ -132,7 +132,7 @@
property public final androidx.glance.text.FontWeight? fontWeight;
}
- @androidx.compose.runtime.Stable public interface GlanceCurvedModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceCurvedModifier {
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
@@ -148,7 +148,7 @@
method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? super R,? extends R> operation);
}
- public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceCurvedModifier.Element extends androidx.glance.wear.tiles.curved.GlanceCurvedModifier {
method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,java.lang.Boolean> predicate);
method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.wear.tiles.curved.GlanceCurvedModifier.Element,? extends R> operation);
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt
index f82054c..c84930b 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/CurvedRow.kt
@@ -244,6 +244,7 @@
@CurvedScopeMarker
interface CurvedChildScope
+@JvmDefaultWithCompatibility
/** A scope for elements which can only be contained within a [CurvedRow]. */
@CurvedScopeMarker
interface CurvedScope {
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt
index 796b93c..dd08bc9 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/curved/GlanceCurvedModifier.kt
@@ -23,6 +23,7 @@
import androidx.glance.semantics.SemanticsConfiguration
import androidx.glance.semantics.SemanticsPropertyReceiver
+@JvmDefaultWithCompatibility
/**
* An ordered, immutable, collection of modifier element that works with curved components in the
* Glance library.
@@ -73,6 +74,7 @@
if (other === GlanceCurvedModifier) this
else CombinedGlanceCurvedModifier(this, other)
+ @JvmDefaultWithCompatibility
/**
* A single element contained within a [GlanceCurvedModifier] chain.
*/
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 222ec6d..56a9a5b 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -48,7 +48,7 @@
public interface GlanceId {
}
- @androidx.compose.runtime.Stable public interface GlanceModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceModifier {
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
@@ -64,7 +64,7 @@
method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.GlanceModifier.Element,? super R,? extends R> operation);
}
- public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 222ec6d..56a9a5b 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -48,7 +48,7 @@
public interface GlanceId {
}
- @androidx.compose.runtime.Stable public interface GlanceModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceModifier {
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
@@ -64,7 +64,7 @@
method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.GlanceModifier.Element,? super R,? extends R> operation);
}
- public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 222ec6d..56a9a5b 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -48,7 +48,7 @@
public interface GlanceId {
}
- @androidx.compose.runtime.Stable public interface GlanceModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface GlanceModifier {
method public boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
@@ -64,7 +64,7 @@
method public <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.glance.GlanceModifier.Element,? super R,? extends R> operation);
}
- public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GlanceModifier.Element extends androidx.glance.GlanceModifier {
method public default boolean all(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public default boolean any(kotlin.jvm.functions.Function1<? super androidx.glance.GlanceModifier.Element,java.lang.Boolean> predicate);
method public default <R> R! foldIn(R? initial, kotlin.jvm.functions.Function2<? super R,? super androidx.glance.GlanceModifier.Element,? extends R> operation);
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt
index bdafdf3..bf8ffc9 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/GlanceModifier.kt
@@ -17,6 +17,7 @@
import androidx.compose.runtime.Stable
+@JvmDefaultWithCompatibility
/**
* An ordered, immutable, collection of modifier element for the Glance library.
*
@@ -65,6 +66,7 @@
infix fun then(other: GlanceModifier): GlanceModifier =
if (other === GlanceModifier) this else CombinedGlanceModifier(this, other)
+ @JvmDefaultWithCompatibility
/**
* A single element contained within a [GlanceModifier] chain.
*/
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
index cb4c94b..d14b141 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionManager.kt
@@ -29,6 +29,7 @@
import androidx.work.workDataOf
import java.util.concurrent.TimeUnit
+@JvmDefaultWithCompatibility
/**
* [SessionManager] is the entrypoint for Glance surfaces to start a session worker that will handle
* their composition.
diff --git a/gradle.properties b/gradle.properties
index c6660ac..c2082a6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -44,6 +44,8 @@
android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline
# Workaround for b/162074215
android.includeDependencyInfoInApks=false
+# Allow multiple r8 tasks at once because otherwise they can make the critical path longer: b/256187923
+android.r8.maxWorkers=2
kotlin.stdlib.default.dependency=false
# mac targets cannot be built on linux, suppress the warning.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0546b9e..11e4050 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -35,7 +35,7 @@
incap = "0.2"
jcodec = "0.2.5"
kotlin = "1.7.21"
-kotlinBenchmark = "0.4.5"
+kotlinBenchmark = "0.4.6"
kotlinNative = "1.7.21"
kotlinCompileTesting = "1.4.9"
kotlinCoroutines = "1.6.4"
@@ -43,7 +43,7 @@
ksp = "1.7.21-1.0.8"
ktlint = "0.46.0-20220520.192227-74"
leakcanary = "2.8.1"
-metalava = "1.0.0-alpha06"
+metalava = "1.0.0-alpha07"
mockito = "2.25.0"
moshi = "1.13.0"
protobuf = "3.21.8"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 764b8b2e..25322cf 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -809,17 +809,17 @@
<sha256 value="4df94aaeee8d900be431386e31ef44e82a66e57c3ae30866aec2875aff01fe70" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
- <artifact name="kotlinx-benchmark-plugin-0.4.5.jar">
- <sha256 value="64042e32a7c23040980d8b7cb9c2dc2281bf6e19da511d054639f44ba5eaac66" origin="Generated by Gradle because artifact wasn't signed"/>
+ <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.6">
+ <artifact name="kotlinx-benchmark-plugin-0.4.6.jar">
+ <sha256 value="9806179c613658456da5a53cb945138ee0983ecccc9185de22ece2be9c14cfc7" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
- <artifact name="kotlinx-benchmark-plugin-0.4.5.pom">
- <sha256 value="33a06eec241b0a20bc3f37b32574d1aaed34905005daba892a85cd832de14700" origin="Generated by Gradle because artifact wasn't signed"/>
+ <artifact name="kotlinx-benchmark-plugin-0.4.6.module">
+ <sha256 value="b2bcac512573b085113a2ad74ed339b7cdefc49094b342a463b5a51b07ef5e1e" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
- <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.5.pom">
- <sha256 value="b13fc9975c2f5cfb992f423c60be042252a289e0f40390e7da2122ac2dec08a5" origin="Generated by Gradle because artifact wasn't signed"/>
+ <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.6">
+ <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.6.pom">
+ <sha256 value="3f739c3504155f5568c26c5c013aa62dc254daa7fe627d7b43b3448c79d36447" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="org.ow2" name="ow2" version="1.5" androidx:reason="https://gitlab.ow2.org/asm/asm/-/merge_requests/354">
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index bf036fb..7000b35 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -43,7 +43,7 @@
field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
}
- public static interface GLFrontBufferedRenderer.Callback<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
@@ -116,7 +116,7 @@
method @WorkerThread public void onEGLContextDestroyed(androidx.graphics.opengl.egl.EGLManager eglManager);
}
- public static interface GLRenderer.RenderCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLRenderer.RenderCallback {
method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
method @WorkerThread public default android.opengl.EGLSurface? onSurfaceCreated(androidx.graphics.opengl.egl.EGLSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
}
@@ -199,7 +199,7 @@
public static final class EGLManager.Companion {
}
- public interface EGLSpec {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface EGLSpec {
method public int eglClientWaitSyncKHR(androidx.opengl.EGLSyncKHR sync, int flags, long timeoutNanos);
method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.opengl.EGLImageKHR? eglCreateImageFromHardwareBuffer(android.hardware.HardwareBuffer hardwareBuffer);
@@ -336,6 +336,7 @@
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
field public static final androidx.opengl.EGLExt.Companion Companion;
+ field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index bf036fb..7000b35 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -43,7 +43,7 @@
field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
}
- public static interface GLFrontBufferedRenderer.Callback<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
@@ -116,7 +116,7 @@
method @WorkerThread public void onEGLContextDestroyed(androidx.graphics.opengl.egl.EGLManager eglManager);
}
- public static interface GLRenderer.RenderCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLRenderer.RenderCallback {
method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
method @WorkerThread public default android.opengl.EGLSurface? onSurfaceCreated(androidx.graphics.opengl.egl.EGLSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
}
@@ -199,7 +199,7 @@
public static final class EGLManager.Companion {
}
- public interface EGLSpec {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface EGLSpec {
method public int eglClientWaitSyncKHR(androidx.opengl.EGLSyncKHR sync, int flags, long timeoutNanos);
method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.opengl.EGLImageKHR? eglCreateImageFromHardwareBuffer(android.hardware.HardwareBuffer hardwareBuffer);
@@ -336,6 +336,7 @@
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
field public static final androidx.opengl.EGLExt.Companion Companion;
+ field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 8ec6a0b..0ee95a4 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -43,7 +43,7 @@
field public static final androidx.graphics.lowlatency.GLFrontBufferedRenderer.Companion Companion;
}
- public static interface GLFrontBufferedRenderer.Callback<T> {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLFrontBufferedRenderer.Callback<T> {
method @WorkerThread public default void onDoubleBufferedLayerRenderComplete(androidx.graphics.surface.SurfaceControlCompat frontBufferedLayerSurfaceControl, androidx.graphics.surface.SurfaceControlCompat.Transaction transaction);
method @WorkerThread public void onDrawDoubleBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, java.util.Collection<? extends T> params);
method @WorkerThread public void onDrawFrontBufferedLayer(androidx.graphics.opengl.egl.EGLManager eglManager, int bufferWidth, int bufferHeight, float[] transform, T? param);
@@ -116,7 +116,7 @@
method @WorkerThread public void onEGLContextDestroyed(androidx.graphics.opengl.egl.EGLManager eglManager);
}
- public static interface GLRenderer.RenderCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public static interface GLRenderer.RenderCallback {
method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EGLManager eglManager);
method @WorkerThread public default android.opengl.EGLSurface? onSurfaceCreated(androidx.graphics.opengl.egl.EGLSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
}
@@ -200,7 +200,7 @@
public static final class EGLManager.Companion {
}
- public interface EGLSpec {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface EGLSpec {
method public int eglClientWaitSyncKHR(androidx.opengl.EGLSyncKHR sync, int flags, long timeoutNanos);
method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.opengl.EGLImageKHR? eglCreateImageFromHardwareBuffer(android.hardware.HardwareBuffer hardwareBuffer);
@@ -337,6 +337,7 @@
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
field public static final androidx.opengl.EGLExt.Companion Companion;
+ field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index b7c29c1..2e54e92 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -30,6 +30,7 @@
import androidx.annotation.RequiresApi
import androidx.opengl.EGLBindings
import androidx.opengl.EGLExt
+import androidx.opengl.EGLExt.Companion.EGL_ANDROID_CLIENT_BUFFER
import androidx.opengl.EGLExt.Companion.EGL_SYNC_CONDITION_KHR
import androidx.opengl.EGLExt.Companion.EGL_SYNC_FENCE_KHR
import androidx.opengl.EGLExt.Companion.EGL_SYNC_NATIVE_FENCE_ANDROID
@@ -399,9 +400,14 @@
isExtensionSupported(EGL_KHR_IMAGE_BASE)
val androidImageNativeBufferSupported =
isExtensionSupported(EGL_ANDROID_IMAGE_NATIVE_BUFFER)
+ val eglClientBufferSupported =
+ isExtensionSupported(EGL_ANDROID_CLIENT_BUFFER)
// According to EGL spec both these extensions are required in order to support
// eglGetNativeClientBufferAndroid
- if (khrImageBaseSupported && androidImageNativeBufferSupported) {
+ if (khrImageBaseSupported &&
+ androidImageNativeBufferSupported &&
+ eglClientBufferSupported
+ ) {
assertTrue(EGLBindings.nSupportsEglGetNativeClientBufferAndroid())
}
}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index b191913..59f43ce 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -613,6 +613,7 @@
}
}
+ @JvmDefaultWithCompatibility
/**
* Provides callbacks for consumers to draw into the front and double buffered layers as well as
* provide opportunities to synchronize [SurfaceControlCompat.Transaction]s to submit the layers
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index a13a2a9..fbdaf57 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -19,6 +19,7 @@
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.surface.SurfaceControlCompat
+@JvmDefaultWithCompatibility
/**
* Interface used to define a parent for rendering front and double buffered layers.
* This provides the following facilities:
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
index a23a80a..ed18a5b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -299,6 +299,7 @@
fun onEGLContextDestroyed(eglManager: EGLManager)
}
+ @JvmDefaultWithCompatibility
/**
* Interface used for creating an [EGLSurface] with a user defined configuration
* from the provided surface as well as a callback used to render content into the surface
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
index 09f4584..7efd824 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
@@ -39,6 +39,7 @@
import androidx.opengl.EGLImageKHR
import androidx.opengl.EGLSyncKHR
+@JvmDefaultWithCompatibility
/**
* Interface for accessing various EGL facilities independent of EGL versions.
* That is each EGL version implements this specification.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
index 0a8272c..a2ef757 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
@@ -79,6 +79,7 @@
fun build(): SurfaceControlImpl
}
+ @JvmDefaultWithCompatibility
@RequiresApi(Build.VERSION_CODES.KITKAT)
interface Transaction : AutoCloseable {
diff --git a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
index 0f48ee3..a2015a1 100644
--- a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
+++ b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
@@ -184,6 +184,14 @@
const val EGL_KHR_IMAGE_BASE = "EGL_KHR_image_base"
/**
+ * Extension that allows creating an EGLClientBuffer from an Android [HardwareBuffer]
+ * object which can later be used to create an [EGLImageKHR] instance.
+ * See:
+ * https://registry.khronos.org/EGL/extensions/ANDROID/EGL_ANDROID_get_native_client_buffer.txt
+ */
+ const val EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer"
+
+ /**
* Extension that defines a new EGL resource type that is suitable for
* sharing 2D arrays of image data between client APIs, the EGLImage,
* and allows creating EGLImages from EGL native pixmaps.
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index bff896d..0349123 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.connect.client {
- public interface HealthConnectClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
@@ -31,7 +31,7 @@
method public boolean isProviderAvailable(android.content.Context context);
}
- public interface PermissionController {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index bff896d..0349123 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.connect.client {
- public interface HealthConnectClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
@@ -31,7 +31,7 @@
method public boolean isProviderAvailable(android.content.Context context);
}
- public interface PermissionController {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 0bf9f664..3be3faa 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.connect.client {
- public interface HealthConnectClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HealthConnectClient {
method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
@@ -31,7 +31,7 @@
method public boolean isProviderAvailable(android.content.Context context);
}
- public interface PermissionController {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 4bcab1f..c6f6c51 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -43,6 +43,7 @@
import java.io.IOException
import kotlin.reflect.KClass
+@JvmDefaultWithCompatibility
/** Interface to access health and fitness records. */
interface HealthConnectClient {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index a8fd057..e5aa0f7 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -22,6 +22,7 @@
import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal
import androidx.health.connect.client.permission.HealthPermission
+@JvmDefaultWithCompatibility
/** Interface for operations related to permissions. */
interface PermissionController {
diff --git a/health/health-services-client/api/1.0.0-beta02.txt b/health/health-services-client/api/1.0.0-beta02.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/1.0.0-beta02.txt
+++ b/health/health-services-client/api/1.0.0-beta02.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.services.client {
- public interface ExerciseClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
}
- public interface MeasureCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
method public default void onRegistered();
@@ -78,7 +78,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
- public interface PassiveListenerCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
}
- public interface Availability {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
method public int getId();
property public abstract int id;
field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.services.client {
- public interface ExerciseClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
}
- public interface MeasureCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
method public default void onRegistered();
@@ -78,7 +78,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
- public interface PassiveListenerCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
}
- public interface Availability {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
method public int getId();
property public abstract int id;
field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.services.client {
- public interface ExerciseClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
}
- public interface MeasureCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
method public default void onRegistered();
@@ -78,7 +78,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
- public interface PassiveListenerCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
}
- public interface Availability {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
method public int getId();
property public abstract int id;
field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.services.client {
- public interface ExerciseClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
}
- public interface MeasureCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
method public default void onRegistered();
@@ -78,7 +78,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
- public interface PassiveListenerCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
}
- public interface Availability {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
method public int getId();
property public abstract int id;
field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/restricted_1.0.0-beta02.txt b/health/health-services-client/api/restricted_1.0.0-beta02.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta02.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta02.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.services.client {
- public interface ExerciseClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
}
- public interface MeasureCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
method public default void onRegistered();
@@ -78,7 +78,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
- public interface PassiveListenerCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
}
- public interface Availability {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
method public int getId();
property public abstract int id;
field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index d65cfd6..3ca2b00 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.health.services.client {
- public interface ExerciseClient {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
@@ -59,7 +59,7 @@
property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
}
- public interface MeasureCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
method public default void onRegistered();
@@ -78,7 +78,7 @@
method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
}
- public interface PassiveListenerCallback {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
@@ -124,7 +124,7 @@
ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
}
- public interface Availability {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
method public int getId();
property public abstract int id;
field public static final androidx.health.services.client.data.Availability.Companion Companion;
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index 311cdcd..948a4d9 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -31,6 +31,7 @@
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.Executor
+@JvmDefaultWithCompatibility
/** Client which provides a way to subscribe to the health data of a device during an exercise. */
public interface ExerciseClient {
/**
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt
index e648778..0397fca 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureCallback.kt
@@ -22,6 +22,7 @@
import androidx.health.services.client.data.DataType
import androidx.health.services.client.data.DeltaDataType
+@JvmDefaultWithCompatibility
/** Callback for [MeasureClient.registerMeasureCallback]. */
public interface MeasureCallback {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt
index b202724..0efdfc9 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerCallback.kt
@@ -22,6 +22,7 @@
import androidx.health.services.client.data.PassiveGoal
import androidx.health.services.client.data.UserActivityInfo
+@JvmDefaultWithCompatibility
/** A callback for receiving passive monitoring updates. */
public interface PassiveListenerCallback {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
index f4438ca..4b3f682 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
@@ -21,6 +21,7 @@
import androidx.health.services.client.proto.DataProto.Availability.AvailabilityCase
import androidx.health.services.client.proto.DataProto.Availability.DataTypeAvailability as DataTypeAvailabilityProto
+@JvmDefaultWithCompatibility
/** Availability of a [DataType]. */
public interface Availability {
public val id: Int
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index af40a5a..b8e3752 100644
--- a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -53,6 +53,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -157,6 +158,7 @@
}
}
+ @Ignore // b/239415930
@Test
@LargeTest
public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
@@ -166,6 +168,7 @@
doTestForVariousNumberImages(builder);
}
+ @Ignore // b/239415930
@Test
@LargeTest
public void testInputBuffer_Grid_NoHandler() throws Throwable {
@@ -175,6 +178,7 @@
doTestForVariousNumberImages(builder);
}
+ @Ignore // b/239415930
@Test
@LargeTest
public void testInputBuffer_NoGrid_Handler() throws Throwable {
@@ -184,6 +188,7 @@
doTestForVariousNumberImages(builder);
}
+ @Ignore // b/239415930
@Test
@LargeTest
public void testInputBuffer_Grid_Handler() throws Throwable {
diff --git a/libraryversions.toml b/libraryversions.toml
index c5f8065..23d4145 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -135,6 +135,7 @@
VIEWPAGER2 = "1.2.0-alpha01"
WEAR = "1.3.0-alpha04"
WEAR_COMPOSE = "1.2.0-alpha01"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha01"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha01"
diff --git a/lifecycle/integration-tests/incrementality/build.gradle b/lifecycle/integration-tests/incrementality/build.gradle
index f048361..21dcdf0 100644
--- a/lifecycle/integration-tests/incrementality/build.gradle
+++ b/lifecycle/integration-tests/incrementality/build.gradle
@@ -32,7 +32,11 @@
SdkResourceGenerator.generateForHostTest(project)
// lifecycle-common and annotation are the dependencies of lifecycle-compiler
-tasks.findByPath("test").dependsOn(tasks.findByPath(":lifecycle:lifecycle-compiler:publish"),
- tasks.findByPath(":lifecycle:lifecycle-common:publish"),
- tasks.findByPath(":lifecycle:lifecycle-runtime:publish"),
- tasks.findByPath(":annotation:annotation:publish"))
+tasks.findByPath("test").configure {
+ dependsOn(":lifecycle:lifecycle-compiler:publish")
+ dependsOn(":lifecycle:lifecycle-common:publish")
+ dependsOn(":lifecycle:lifecycle-runtime:publish")
+ dependsOn(":annotation:annotation:publish")
+ dependsOn(":arch:core:core-common:publish")
+ dependsOn(":arch:core:core-runtime:publish")
+}
diff --git a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
index 43a6a72..5b4d60d 100644
--- a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
+++ b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
@@ -36,7 +36,7 @@
fun initialization() {
val owner = TestLifecycleOwner(Lifecycle.State.INITIALIZED, UnconfinedTestDispatcher())
val scope = owner.lifecycleScope
- assertThat(owner.lifecycle.mInternalScopeRef.get()).isSameInstanceAs(scope)
+ assertThat(owner.lifecycle.internalScopeRef.get()).isSameInstanceAs(scope)
val scope2 = owner.lifecycleScope
assertThat(scope).isSameInstanceAs(scope2)
runBlocking(Dispatchers.Main) {
diff --git a/lifecycle/lifecycle-common/api/current.txt b/lifecycle/lifecycle-common/api/current.txt
index 53075b6..a339f50 100644
--- a/lifecycle/lifecycle-common/api/current.txt
+++ b/lifecycle/lifecycle-common/api/current.txt
@@ -12,17 +12,21 @@
public abstract class Lifecycle {
ctor public Lifecycle();
- method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver);
+ method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
- method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver);
+ method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
}
public enum Lifecycle.Event {
- method public static androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State);
- method public static androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State);
- method public androidx.lifecycle.Lifecycle.State getTargetState();
- method public static androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State);
- method public static androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State);
+ method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public final androidx.lifecycle.Lifecycle.State getTargetState();
+ method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.Event[] values();
+ property public final androidx.lifecycle.Lifecycle.State targetState;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
@@ -30,10 +34,20 @@
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+ field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+ }
+
+ public static final class Lifecycle.Event.Companion {
+ method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
}
public enum Lifecycle.State {
- method public boolean isAtLeast(androidx.lifecycle.Lifecycle.State);
+ method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.State[] values();
enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
diff --git a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
index 53075b6..a339f50 100644
--- a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
@@ -12,17 +12,21 @@
public abstract class Lifecycle {
ctor public Lifecycle();
- method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver);
+ method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
- method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver);
+ method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
}
public enum Lifecycle.Event {
- method public static androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State);
- method public static androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State);
- method public androidx.lifecycle.Lifecycle.State getTargetState();
- method public static androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State);
- method public static androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State);
+ method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public final androidx.lifecycle.Lifecycle.State getTargetState();
+ method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.Event[] values();
+ property public final androidx.lifecycle.Lifecycle.State targetState;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
@@ -30,10 +34,20 @@
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+ field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+ }
+
+ public static final class Lifecycle.Event.Companion {
+ method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
}
public enum Lifecycle.State {
- method public boolean isAtLeast(androidx.lifecycle.Lifecycle.State);
+ method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.State[] values();
enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
diff --git a/lifecycle/lifecycle-common/api/restricted_current.txt b/lifecycle/lifecycle-common/api/restricted_current.txt
index 86f017b..04b9c90 100644
--- a/lifecycle/lifecycle-common/api/restricted_current.txt
+++ b/lifecycle/lifecycle-common/api/restricted_current.txt
@@ -19,17 +19,21 @@
public abstract class Lifecycle {
ctor public Lifecycle();
- method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver);
+ method @MainThread public abstract void addObserver(androidx.lifecycle.LifecycleObserver observer);
method @MainThread public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
- method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver);
+ method @MainThread public abstract void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ property @MainThread public abstract androidx.lifecycle.Lifecycle.State currentState;
}
public enum Lifecycle.Event {
- method public static androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State);
- method public static androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State);
- method public androidx.lifecycle.Lifecycle.State getTargetState();
- method public static androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State);
- method public static androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State);
+ method public static final androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public final androidx.lifecycle.Lifecycle.State getTargetState();
+ method public static final androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public static final androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.Event valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.Event[] values();
+ property public final androidx.lifecycle.Lifecycle.State targetState;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_ANY;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_CREATE;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_DESTROY;
@@ -37,10 +41,20 @@
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_RESUME;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_START;
enum_constant public static final androidx.lifecycle.Lifecycle.Event ON_STOP;
+ field public static final androidx.lifecycle.Lifecycle.Event.Companion Companion;
+ }
+
+ public static final class Lifecycle.Event.Companion {
+ method public androidx.lifecycle.Lifecycle.Event? downFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? downTo(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upFrom(androidx.lifecycle.Lifecycle.State state);
+ method public androidx.lifecycle.Lifecycle.Event? upTo(androidx.lifecycle.Lifecycle.State state);
}
public enum Lifecycle.State {
- method public boolean isAtLeast(androidx.lifecycle.Lifecycle.State);
+ method public final boolean isAtLeast(androidx.lifecycle.Lifecycle.State state);
+ method public static androidx.lifecycle.Lifecycle.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.lifecycle.Lifecycle.State[] values();
enum_constant public static final androidx.lifecycle.Lifecycle.State CREATED;
enum_constant public static final androidx.lifecycle.Lifecycle.State DESTROYED;
enum_constant public static final androidx.lifecycle.Lifecycle.State INITIALIZED;
@@ -59,13 +73,14 @@
method public androidx.lifecycle.Lifecycle getLifecycle();
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Lifecycling {
- method public static String getAdapterName(String);
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Lifecycling {
+ method public static String getAdapterName(String className);
+ method public static androidx.lifecycle.LifecycleEventObserver lifecycleEventObserver(Object object);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MethodCallsLogger {
ctor public MethodCallsLogger();
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean approveCall(String, int);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean approveCall(String name, int type);
}
@Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface OnLifecycleEvent {
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index 71c6829..061f3b0 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -44,11 +44,3 @@
inceptionYear = "2017"
description = "Android Lifecycle-Common"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.java
deleted file mode 100644
index 0a5efd9..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.java
+++ /dev/null
@@ -1,298 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Defines an object that has an Android Lifecycle. {@link androidx.fragment.app.Fragment Fragment}
- * and {@link androidx.fragment.app.FragmentActivity FragmentActivity} classes implement
- * {@link LifecycleOwner} interface which has the {@link LifecycleOwner#getLifecycle()
- * getLifecycle} method to access the Lifecycle. You can also implement {@link LifecycleOwner}
- * in your own classes.
- * <p>
- * {@link Event#ON_CREATE}, {@link Event#ON_START}, {@link Event#ON_RESUME} events in this class
- * are dispatched <b>after</b> the {@link LifecycleOwner}'s related method returns.
- * {@link Event#ON_PAUSE}, {@link Event#ON_STOP}, {@link Event#ON_DESTROY} events in this class
- * are dispatched <b>before</b> the {@link LifecycleOwner}'s related method is called.
- * For instance, {@link Event#ON_START} will be dispatched after
- * {@link android.app.Activity#onStart onStart} returns, {@link Event#ON_STOP} will be dispatched
- * before {@link android.app.Activity#onStop onStop} is called.
- * This gives you certain guarantees on which state the owner is in.
- * <p>
- * To observe lifecycle events call {@link #addObserver(LifecycleObserver)} passing an object
- * that implements either {@link DefaultLifecycleObserver} or {@link LifecycleEventObserver}.
- */
-public abstract class Lifecycle {
-
- /**
- * Lifecycle coroutines extensions stashes the CoroutineScope into this field.
- *
- * @hide used by lifecycle-common-ktx
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- AtomicReference<Object> mInternalScopeRef = new AtomicReference<>();
-
- /**
- * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
- * state.
- * <p>
- * The given observer will be brought to the current state of the LifecycleOwner.
- * For example, if the LifecycleOwner is in {@link State#STARTED} state, the given observer
- * will receive {@link Event#ON_CREATE}, {@link Event#ON_START} events.
- *
- * @param observer The observer to notify.
- */
- @MainThread
- public abstract void addObserver(@NonNull LifecycleObserver observer);
-
- /**
- * Removes the given observer from the observers list.
- * <p>
- * If this method is called while a state change is being dispatched,
- * <ul>
- * <li>If the given observer has not yet received that event, it will not receive it.
- * <li>If the given observer has more than 1 method that observes the currently dispatched
- * event and at least one of them received the event, all of them will receive the event and
- * the removal will happen afterwards.
- * </ul>
- *
- * @param observer The observer to be removed.
- */
- @MainThread
- public abstract void removeObserver(@NonNull LifecycleObserver observer);
-
- /**
- * Returns the current state of the Lifecycle.
- *
- * @return The current state of the Lifecycle.
- */
- @MainThread
- @NonNull
- public abstract State getCurrentState();
-
- @SuppressWarnings("WeakerAccess")
- public enum Event {
- /**
- * Constant for onCreate event of the {@link LifecycleOwner}.
- */
- ON_CREATE,
- /**
- * Constant for onStart event of the {@link LifecycleOwner}.
- */
- ON_START,
- /**
- * Constant for onResume event of the {@link LifecycleOwner}.
- */
- ON_RESUME,
- /**
- * Constant for onPause event of the {@link LifecycleOwner}.
- */
- ON_PAUSE,
- /**
- * Constant for onStop event of the {@link LifecycleOwner}.
- */
- ON_STOP,
- /**
- * Constant for onDestroy event of the {@link LifecycleOwner}.
- */
- ON_DESTROY,
- /**
- * An {@link Event Event} constant that can be used to match all events.
- */
- ON_ANY;
-
- /**
- * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
- * leaving the specified {@link Lifecycle.State} to a lower state, or {@code null}
- * if there is no valid event that can move down from the given state.
- *
- * @param state the higher state that the returned event will transition down from
- * @return the event moving down the lifecycle phases from state
- */
- @Nullable
- public static Event downFrom(@NonNull State state) {
- switch (state) {
- case CREATED:
- return ON_DESTROY;
- case STARTED:
- return ON_STOP;
- case RESUMED:
- return ON_PAUSE;
- default:
- return null;
- }
- }
-
- /**
- * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
- * entering the specified {@link Lifecycle.State} from a higher state, or {@code null}
- * if there is no valid event that can move down to the given state.
- *
- * @param state the lower state that the returned event will transition down to
- * @return the event moving down the lifecycle phases to state
- */
- @Nullable
- public static Event downTo(@NonNull State state) {
- switch (state) {
- case DESTROYED:
- return ON_DESTROY;
- case CREATED:
- return ON_STOP;
- case STARTED:
- return ON_PAUSE;
- default:
- return null;
- }
- }
-
- /**
- * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
- * leaving the specified {@link Lifecycle.State} to a higher state, or {@code null}
- * if there is no valid event that can move up from the given state.
- *
- * @param state the lower state that the returned event will transition up from
- * @return the event moving up the lifecycle phases from state
- */
- @Nullable
- public static Event upFrom(@NonNull State state) {
- switch (state) {
- case INITIALIZED:
- return ON_CREATE;
- case CREATED:
- return ON_START;
- case STARTED:
- return ON_RESUME;
- default:
- return null;
- }
- }
-
- /**
- * Returns the {@link Lifecycle.Event} that will be reported by a {@link Lifecycle}
- * entering the specified {@link Lifecycle.State} from a lower state, or {@code null}
- * if there is no valid event that can move up to the given state.
- *
- * @param state the higher state that the returned event will transition up to
- * @return the event moving up the lifecycle phases to state
- */
- @Nullable
- public static Event upTo(@NonNull State state) {
- switch (state) {
- case CREATED:
- return ON_CREATE;
- case STARTED:
- return ON_START;
- case RESUMED:
- return ON_RESUME;
- default:
- return null;
- }
- }
-
- /**
- * Returns the new {@link Lifecycle.State} of a {@link Lifecycle} that just reported
- * this {@link Lifecycle.Event}.
- *
- * Throws {@link IllegalArgumentException} if called on {@link #ON_ANY}, as it is a special
- * value used by {@link OnLifecycleEvent} and not a real lifecycle event.
- *
- * @return the state that will result from this event
- */
- @NonNull
- public State getTargetState() {
- switch (this) {
- case ON_CREATE:
- case ON_STOP:
- return State.CREATED;
- case ON_START:
- case ON_PAUSE:
- return State.STARTED;
- case ON_RESUME:
- return State.RESUMED;
- case ON_DESTROY:
- return State.DESTROYED;
- case ON_ANY:
- break;
- }
- throw new IllegalArgumentException(this + " has no target state");
- }
- }
-
- /**
- * Lifecycle states. You can consider the states as the nodes in a graph and
- * {@link Event}s as the edges between these nodes.
- */
- @SuppressWarnings("WeakerAccess")
- public enum State {
- /**
- * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
- * any more events. For instance, for an {@link android.app.Activity}, this state is reached
- * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
- */
- DESTROYED,
-
- /**
- * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
- * the state when it is constructed but has not received
- * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
- */
- INITIALIZED,
-
- /**
- * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
- * is reached in two cases:
- * <ul>
- * <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
- * <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
- * </ul>
- */
- CREATED,
-
- /**
- * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
- * is reached in two cases:
- * <ul>
- * <li>after {@link android.app.Activity#onStart() onStart} call;
- * <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
- * </ul>
- */
- STARTED,
-
- /**
- * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
- * is reached after {@link android.app.Activity#onResume() onResume} is called.
- */
- RESUMED;
-
- /**
- * Compares if this State is greater or equal to the given {@code state}.
- *
- * @param state State to compare with
- * @return true if this State is greater or equal to the given {@code state}
- */
- public boolean isAtLeast(@NonNull State state) {
- return compareTo(state) >= 0;
- }
- }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
new file mode 100644
index 0000000..a0e6648
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import androidx.annotation.MainThread
+import androidx.annotation.RestrictTo
+import androidx.lifecycle.Lifecycle.Event
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Defines an object that has an Android Lifecycle. [Fragment][androidx.fragment.app.Fragment]
+ * and [FragmentActivity][androidx.fragment.app.FragmentActivity] classes implement
+ * [LifecycleOwner] interface which has the [ getLifecycle][LifecycleOwner.getLifecycle] method to access the Lifecycle. You can also implement [LifecycleOwner]
+ * in your own classes.
+ *
+ * [Event.ON_CREATE], [Event.ON_START], [Event.ON_RESUME] events in this class
+ * are dispatched **after** the [LifecycleOwner]'s related method returns.
+ * [Event.ON_PAUSE], [Event.ON_STOP], [Event.ON_DESTROY] events in this class
+ * are dispatched **before** the [LifecycleOwner]'s related method is called.
+ * For instance, [Event.ON_START] will be dispatched after
+ * [onStart][android.app.Activity.onStart] returns, [Event.ON_STOP] will be dispatched
+ * before [onStop][android.app.Activity.onStop] is called.
+ * This gives you certain guarantees on which state the owner is in.
+ *
+ * To observe lifecycle events call [.addObserver] passing an object
+ * that implements either [DefaultLifecycleObserver] or [LifecycleEventObserver].
+ */
+public abstract class Lifecycle {
+ /**
+ * Lifecycle coroutines extensions stashes the CoroutineScope into this field.
+ *
+ * @hide used by lifecycle-common-ktx
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()
+
+ /**
+ * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
+ * state.
+ *
+ * The given observer will be brought to the current state of the LifecycleOwner.
+ * For example, if the LifecycleOwner is in [State.STARTED] state, the given observer
+ * will receive [Event.ON_CREATE], [Event.ON_START] events.
+ *
+ * @param observer The observer to notify.
+ */
+ @MainThread
+ public abstract fun addObserver(observer: LifecycleObserver)
+
+ /**
+ * Removes the given observer from the observers list.
+ *
+ * If this method is called while a state change is being dispatched,
+ *
+ * * If the given observer has not yet received that event, it will not receive it.
+ * * If the given observer has more than 1 method that observes the currently dispatched
+ * event and at least one of them received the event, all of them will receive the event and
+ * the removal will happen afterwards.
+ *
+ *
+ * @param observer The observer to be removed.
+ */
+ @MainThread
+ public abstract fun removeObserver(observer: LifecycleObserver)
+
+ /**
+ * Returns the current state of the Lifecycle.
+ *
+ * @return The current state of the Lifecycle.
+ */
+ @get:MainThread
+ public abstract val currentState: State
+
+ public enum class Event {
+ /**
+ * Constant for onCreate event of the [LifecycleOwner].
+ */
+ ON_CREATE,
+
+ /**
+ * Constant for onStart event of the [LifecycleOwner].
+ */
+ ON_START,
+
+ /**
+ * Constant for onResume event of the [LifecycleOwner].
+ */
+ ON_RESUME,
+
+ /**
+ * Constant for onPause event of the [LifecycleOwner].
+ */
+ ON_PAUSE,
+
+ /**
+ * Constant for onStop event of the [LifecycleOwner].
+ */
+ ON_STOP,
+
+ /**
+ * Constant for onDestroy event of the [LifecycleOwner].
+ */
+ ON_DESTROY,
+
+ /**
+ * An [Event] constant that can be used to match all events.
+ */
+ ON_ANY;
+
+ /**
+ * Returns the new [Lifecycle.State] of a [Lifecycle] that just reported
+ * this [Lifecycle.Event].
+ *
+ * Throws [IllegalArgumentException] if called on [.ON_ANY], as it is a special
+ * value used by [OnLifecycleEvent] and not a real lifecycle event.
+ *
+ * @return the state that will result from this event
+ */
+ public val targetState: State
+ get() {
+ when (this) {
+ ON_CREATE, ON_STOP -> return State.CREATED
+ ON_START, ON_PAUSE -> return State.STARTED
+ ON_RESUME -> return State.RESUMED
+ ON_DESTROY -> return State.DESTROYED
+ ON_ANY -> {}
+ }
+ throw IllegalArgumentException("$this has no target state")
+ }
+
+ public companion object {
+ /**
+ * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+ * leaving the specified [Lifecycle.State] to a lower state, or `null`
+ * if there is no valid event that can move down from the given state.
+ *
+ * @param state the higher state that the returned event will transition down from
+ * @return the event moving down the lifecycle phases from state
+ */
+ @JvmStatic
+ public fun downFrom(state: State): Event? {
+ return when (state) {
+ State.CREATED -> ON_DESTROY
+ State.STARTED -> ON_STOP
+ State.RESUMED -> ON_PAUSE
+ else -> null
+ }
+ }
+
+ /**
+ * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+ * entering the specified [Lifecycle.State] from a higher state, or `null`
+ * if there is no valid event that can move down to the given state.
+ *
+ * @param state the lower state that the returned event will transition down to
+ * @return the event moving down the lifecycle phases to state
+ */
+ @JvmStatic
+ public fun downTo(state: State): Event? {
+ return when (state) {
+ State.DESTROYED -> ON_DESTROY
+ State.CREATED -> ON_STOP
+ State.STARTED -> ON_PAUSE
+ else -> null
+ }
+ }
+
+ /**
+ * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+ * leaving the specified [Lifecycle.State] to a higher state, or `null`
+ * if there is no valid event that can move up from the given state.
+ *
+ * @param state the lower state that the returned event will transition up from
+ * @return the event moving up the lifecycle phases from state
+ */
+ @JvmStatic
+ public fun upFrom(state: State): Event? {
+ return when (state) {
+ State.INITIALIZED -> ON_CREATE
+ State.CREATED -> ON_START
+ State.STARTED -> ON_RESUME
+ else -> null
+ }
+ }
+
+ /**
+ * Returns the [Lifecycle.Event] that will be reported by a [Lifecycle]
+ * entering the specified [Lifecycle.State] from a lower state, or `null`
+ * if there is no valid event that can move up to the given state.
+ *
+ * @param state the higher state that the returned event will transition up to
+ * @return the event moving up the lifecycle phases to state
+ */
+ @JvmStatic
+ public fun upTo(state: State): Event? {
+ return when (state) {
+ State.CREATED -> ON_CREATE
+ State.STARTED -> ON_START
+ State.RESUMED -> ON_RESUME
+ else -> null
+ }
+ }
+ }
+ }
+
+ /**
+ * Lifecycle states. You can consider the states as the nodes in a graph and
+ * [Event]s as the edges between these nodes.
+ */
+ public enum class State {
+ /**
+ * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
+ * any more events. For instance, for an [android.app.Activity], this state is reached
+ * **right before** Activity's [onDestroy][android.app.Activity.onDestroy] call.
+ */
+ DESTROYED,
+
+ /**
+ * Initialized state for a LifecycleOwner. For an [android.app.Activity], this is
+ * the state when it is constructed but has not received
+ * [onCreate][android.app.Activity.onCreate] yet.
+ */
+ INITIALIZED,
+
+ /**
+ * Created state for a LifecycleOwner. For an [android.app.Activity], this state
+ * is reached in two cases:
+ *
+ * * after [onCreate][android.app.Activity.onCreate] call;
+ * * **right before** [onStop][android.app.Activity.onStop] call.
+ *
+ */
+ CREATED,
+
+ /**
+ * Started state for a LifecycleOwner. For an [android.app.Activity], this state
+ * is reached in two cases:
+ *
+ * * after [onStart][android.app.Activity.onStart] call;
+ * * **right before** [onPause][android.app.Activity.onPause] call.
+ *
+ */
+ STARTED,
+
+ /**
+ * Resumed state for a LifecycleOwner. For an [android.app.Activity], this state
+ * is reached after [onResume][android.app.Activity.onResume] is called.
+ */
+ RESUMED;
+
+ /**
+ * Compares if this State is greater or equal to the given `state`.
+ *
+ * @param state State to compare with
+ * @return true if this State is greater or equal to the given `state`
+ */
+ public fun isAtLeast(state: State): Boolean {
+ return compareTo(state) >= 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
deleted file mode 100644
index 722f0bf..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Internal class to handle lifecycle conversion etc.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class Lifecycling {
-
- private static final int REFLECTIVE_CALLBACK = 1;
- private static final int GENERATED_CALLBACK = 2;
-
- private static Map<Class<?>, Integer> sCallbackCache = new HashMap<>();
- private static Map<Class<?>, List<Constructor<? extends GeneratedAdapter>>> sClassToAdapters =
- new HashMap<>();
-
- // Left for binary compatibility when lifecycle-common goes up 2.1 as transitive dep
- // but lifecycle-runtime stays 2.0
-
- /**
- * @deprecated Left for compatibility with lifecycle-runtime:2.0
- */
- @SuppressWarnings("deprecation")
- @Deprecated
- @NonNull
- static GenericLifecycleObserver getCallback(final Object object) {
- final LifecycleEventObserver observer = lifecycleEventObserver(object);
- return new GenericLifecycleObserver() {
- @Override
- public void onStateChanged(@NonNull LifecycleOwner source,
- @NonNull Lifecycle.Event event) {
- observer.onStateChanged(source, event);
- }
- };
- }
-
- @NonNull
- @SuppressWarnings("deprecation")
- static LifecycleEventObserver lifecycleEventObserver(Object object) {
- boolean isLifecycleEventObserver = object instanceof LifecycleEventObserver;
- boolean isDefaultLifecycleObserver = object instanceof DefaultLifecycleObserver;
- if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
- return new DefaultLifecycleObserverAdapter((DefaultLifecycleObserver) object,
- (LifecycleEventObserver) object);
- }
- if (isDefaultLifecycleObserver) {
- return new DefaultLifecycleObserverAdapter((DefaultLifecycleObserver) object, null);
- }
-
- if (isLifecycleEventObserver) {
- return (LifecycleEventObserver) object;
- }
-
- final Class<?> klass = object.getClass();
- int type = getObserverConstructorType(klass);
- if (type == GENERATED_CALLBACK) {
- List<Constructor<? extends GeneratedAdapter>> constructors =
- sClassToAdapters.get(klass);
- if (constructors.size() == 1) {
- GeneratedAdapter generatedAdapter = createGeneratedAdapter(
- constructors.get(0), object);
- return new SingleGeneratedAdapterObserver(generatedAdapter);
- }
- GeneratedAdapter[] adapters = new GeneratedAdapter[constructors.size()];
- for (int i = 0; i < constructors.size(); i++) {
- adapters[i] = createGeneratedAdapter(constructors.get(i), object);
- }
- return new CompositeGeneratedAdaptersObserver(adapters);
- }
- return new ReflectiveGenericLifecycleObserver(object);
- }
-
- private static GeneratedAdapter createGeneratedAdapter(
- Constructor<? extends GeneratedAdapter> constructor, Object object) {
- //noinspection TryWithIdenticalCatches
- try {
- return constructor.newInstance(object);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- } catch (InstantiationException e) {
- throw new RuntimeException(e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e);
- }
- }
-
- @SuppressWarnings("deprecation")
- @Nullable
- private static Constructor<? extends GeneratedAdapter> generatedConstructor(Class<?> klass) {
- try {
- Package aPackage = klass.getPackage();
- String name = klass.getCanonicalName();
- final String fullPackage = aPackage != null ? aPackage.getName() : "";
- final String adapterName = getAdapterName(fullPackage.isEmpty() ? name :
- name.substring(fullPackage.length() + 1));
-
- @SuppressWarnings("unchecked") final Class<? extends GeneratedAdapter> aClass =
- (Class<? extends GeneratedAdapter>) Class.forName(
- fullPackage.isEmpty() ? adapterName : fullPackage + "." + adapterName);
- Constructor<? extends GeneratedAdapter> constructor =
- aClass.getDeclaredConstructor(klass);
- if (!constructor.isAccessible()) {
- constructor.setAccessible(true);
- }
- return constructor;
- } catch (ClassNotFoundException e) {
- return null;
- } catch (NoSuchMethodException e) {
- // this should not happen
- throw new RuntimeException(e);
- }
- }
-
- private static int getObserverConstructorType(Class<?> klass) {
- Integer callbackCache = sCallbackCache.get(klass);
- if (callbackCache != null) {
- return callbackCache;
- }
- int type = resolveObserverCallbackType(klass);
- sCallbackCache.put(klass, type);
- return type;
- }
-
- private static int resolveObserverCallbackType(Class<?> klass) {
- // anonymous class bug:35073837
- if (klass.getCanonicalName() == null) {
- return REFLECTIVE_CALLBACK;
- }
-
- Constructor<? extends GeneratedAdapter> constructor = generatedConstructor(klass);
- if (constructor != null) {
- sClassToAdapters.put(klass, Collections
- .<Constructor<? extends GeneratedAdapter>>singletonList(constructor));
- return GENERATED_CALLBACK;
- }
-
- @SuppressWarnings("deprecation")
- boolean hasLifecycleMethods = ClassesInfoCache.sInstance.hasLifecycleMethods(klass);
- if (hasLifecycleMethods) {
- return REFLECTIVE_CALLBACK;
- }
-
- Class<?> superclass = klass.getSuperclass();
- List<Constructor<? extends GeneratedAdapter>> adapterConstructors = null;
- if (isLifecycleParent(superclass)) {
- if (getObserverConstructorType(superclass) == REFLECTIVE_CALLBACK) {
- return REFLECTIVE_CALLBACK;
- }
- adapterConstructors = new ArrayList<>(sClassToAdapters.get(superclass));
- }
-
- for (Class<?> intrface : klass.getInterfaces()) {
- if (!isLifecycleParent(intrface)) {
- continue;
- }
- if (getObserverConstructorType(intrface) == REFLECTIVE_CALLBACK) {
- return REFLECTIVE_CALLBACK;
- }
- if (adapterConstructors == null) {
- adapterConstructors = new ArrayList<>();
- }
- adapterConstructors.addAll(sClassToAdapters.get(intrface));
- }
- if (adapterConstructors != null) {
- sClassToAdapters.put(klass, adapterConstructors);
- return GENERATED_CALLBACK;
- }
-
- return REFLECTIVE_CALLBACK;
- }
-
- private static boolean isLifecycleParent(Class<?> klass) {
- return klass != null && LifecycleObserver.class.isAssignableFrom(klass);
- }
-
- /**
- * Create a name for an adapter class.
- */
- @NonNull
- public static String getAdapterName(@NonNull String className) {
- return className.replace(".", "_") + "_LifecycleAdapter";
- }
-
- private Lifecycling() {
- }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
new file mode 100644
index 0000000..f3a1b9f
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import androidx.annotation.RestrictTo
+import java.lang.reflect.Constructor
+import java.lang.reflect.InvocationTargetException
+
+/**
+ * Internal class to handle lifecycle conversion etc.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public object Lifecycling {
+ private const val REFLECTIVE_CALLBACK = 1
+ private const val GENERATED_CALLBACK = 2
+ private val callbackCache: MutableMap<Class<*>, Int> = HashMap()
+ private val classToAdapters: MutableMap<Class<*>, List<Constructor<out GeneratedAdapter>>> =
+ HashMap()
+
+ @JvmStatic
+ @Suppress("DEPRECATION")
+ public fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
+ val isLifecycleEventObserver = `object` is LifecycleEventObserver
+ val isDefaultLifecycleObserver = `object` is DefaultLifecycleObserver
+ if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
+ return DefaultLifecycleObserverAdapter(
+ `object` as DefaultLifecycleObserver,
+ `object` as LifecycleEventObserver
+ )
+ }
+ if (isDefaultLifecycleObserver) {
+ return DefaultLifecycleObserverAdapter(`object` as DefaultLifecycleObserver, null)
+ }
+ if (isLifecycleEventObserver) {
+ return `object` as LifecycleEventObserver
+ }
+ val klass: Class<*> = `object`.javaClass
+ val type = getObserverConstructorType(klass)
+ if (type == GENERATED_CALLBACK) {
+ val constructors = classToAdapters[klass]!!
+ if (constructors.size == 1) {
+ val generatedAdapter = createGeneratedAdapter(
+ constructors[0], `object`
+ )
+ return SingleGeneratedAdapterObserver(generatedAdapter)
+ }
+ val adapters: Array<GeneratedAdapter> = Array(constructors.size) { i ->
+ createGeneratedAdapter(constructors[i], `object`)
+ }
+ return CompositeGeneratedAdaptersObserver(adapters)
+ }
+ return ReflectiveGenericLifecycleObserver(`object`)
+ }
+
+ private fun createGeneratedAdapter(
+ constructor: Constructor<out GeneratedAdapter>,
+ `object`: Any
+ ): GeneratedAdapter {
+ return try {
+ constructor.newInstance(`object`)
+ } catch (e: IllegalAccessException) {
+ throw RuntimeException(e)
+ } catch (e: InstantiationException) {
+ throw RuntimeException(e)
+ } catch (e: InvocationTargetException) {
+ throw RuntimeException(e)
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun generatedConstructor(klass: Class<*>): Constructor<out GeneratedAdapter>? {
+ return try {
+ val aPackage = klass.getPackage()
+ val name = klass.canonicalName
+ val fullPackage = if (aPackage != null) aPackage.name else ""
+ val adapterName =
+ getAdapterName(
+ if (fullPackage.isEmpty()) name
+ else name.substring(fullPackage.length + 1)
+ )
+ @Suppress("UNCHECKED_CAST")
+ val aClass = Class.forName(
+ if (fullPackage.isEmpty()) adapterName else "$fullPackage.$adapterName"
+ ) as Class<out GeneratedAdapter>
+ val constructor = aClass.getDeclaredConstructor(klass)
+ if (!constructor.isAccessible) {
+ constructor.isAccessible = true
+ }
+ constructor
+ } catch (e: ClassNotFoundException) {
+ null
+ } catch (e: NoSuchMethodException) {
+ // this should not happen
+ throw RuntimeException(e)
+ }
+ }
+
+ private fun getObserverConstructorType(klass: Class<*>): Int {
+ val callbackCache = callbackCache[klass]
+ if (callbackCache != null) {
+ return callbackCache
+ }
+ val type = resolveObserverCallbackType(klass)
+ this.callbackCache[klass] = type
+ return type
+ }
+
+ private fun resolveObserverCallbackType(klass: Class<*>): Int {
+ // anonymous class bug:35073837
+ if (klass.canonicalName == null) {
+ return REFLECTIVE_CALLBACK
+ }
+ val constructor = generatedConstructor(klass)
+ if (constructor != null) {
+ classToAdapters[klass] = listOf(constructor)
+ return GENERATED_CALLBACK
+ }
+ @Suppress("DEPRECATION")
+ val hasLifecycleMethods = ClassesInfoCache.sInstance.hasLifecycleMethods(klass)
+ if (hasLifecycleMethods) {
+ return REFLECTIVE_CALLBACK
+ }
+ val superclass = klass.superclass
+ var adapterConstructors: MutableList<Constructor<out GeneratedAdapter>>? = null
+ if (isLifecycleParent(superclass)) {
+ if (getObserverConstructorType(superclass) == REFLECTIVE_CALLBACK) {
+ return REFLECTIVE_CALLBACK
+ }
+ adapterConstructors = ArrayList(
+ classToAdapters[superclass]!!
+ )
+ }
+ for (intrface in klass.interfaces) {
+ if (!isLifecycleParent(intrface)) {
+ continue
+ }
+ if (getObserverConstructorType(intrface) == REFLECTIVE_CALLBACK) {
+ return REFLECTIVE_CALLBACK
+ }
+ if (adapterConstructors == null) {
+ adapterConstructors = ArrayList()
+ }
+ adapterConstructors.addAll(classToAdapters[intrface]!!)
+ }
+ if (adapterConstructors != null) {
+ classToAdapters[klass] = adapterConstructors
+ return GENERATED_CALLBACK
+ }
+ return REFLECTIVE_CALLBACK
+ }
+
+ private fun isLifecycleParent(klass: Class<*>?): Boolean {
+ return klass != null && LifecycleObserver::class.java.isAssignableFrom(klass)
+ }
+
+ /**
+ * Create a name for an adapter class.
+ */
+ @JvmStatic
+ public fun getAdapterName(className: String): String {
+ return className.replace(".", "_") + "_LifecycleAdapter"
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.java
deleted file mode 100644
index d72a3ac..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class MethodCallsLogger {
- private Map<String, Integer> mCalledMethods = new HashMap<>();
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public boolean approveCall(@NonNull String name, int type) {
- Integer nullableMask = mCalledMethods.get(name);
- int mask = nullableMask != null ? nullableMask : 0;
- boolean wasCalled = (mask & type) != 0;
- mCalledMethods.put(name, mask | type);
- return !wasCalled;
- }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt
new file mode 100644
index 0000000..c14c15e
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import androidx.annotation.RestrictTo
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public open class MethodCallsLogger() {
+ private val calledMethods: MutableMap<String, Int> = HashMap()
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public open fun approveCall(name: String, type: Int): Boolean {
+ val nullableMask = calledMethods[name]
+ val mask = nullableMask ?: 0
+ val wasCalled = mask and type != 0
+ calledMethods[name] = mask or type
+ return !wasCalled
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java
index 0fc7f0e..44e7ee5 100644
--- a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java
+++ b/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.java
@@ -101,20 +101,6 @@
assertThat(observer, is(observer));
}
- // MUST BE HERE TILL Lifecycle 3.0.0 release for back-compatibility with other modules
- @SuppressWarnings("deprecation")
- @Test
- public void testDeprecatedLifecyclingCallback() {
- GenericLifecycleObserver genericLifecycleObserver = new GenericLifecycleObserver() {
- @Override
- public void onStateChanged(@NonNull LifecycleOwner source,
- @NonNull Lifecycle.Event event) {
- }
- };
- LifecycleEventObserver observer = Lifecycling.getCallback(genericLifecycleObserver);
- assertThat(observer, is(observer));
- }
-
@Test
public void defaultLifecycleObserverAndAnnotations() {
class AnnotatedFullLifecycleObserver implements DefaultLifecycleObserver {
diff --git a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt
index 1bb682c..028af8a 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/Lifecycle.kt
@@ -35,7 +35,7 @@
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
- val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
+ val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
@@ -43,7 +43,7 @@
this,
SupervisorJob() + Dispatchers.Main.immediate
)
- if (mInternalScopeRef.compareAndSet(null, newScope)) {
+ if (internalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
diff --git a/lifecycle/lifecycle-runtime/api/current.txt b/lifecycle/lifecycle-runtime/api/current.txt
index 5bbe95f..c7da237 100644
--- a/lifecycle/lifecycle-runtime/api/current.txt
+++ b/lifecycle/lifecycle-runtime/api/current.txt
@@ -2,15 +2,22 @@
package androidx.lifecycle {
public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
- ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
- method public void addObserver(androidx.lifecycle.LifecycleObserver);
- method @VisibleForTesting public static androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner);
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
method public androidx.lifecycle.Lifecycle.State getCurrentState();
method public int getObserverCount();
- method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
- method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State);
- method public void removeObserver(androidx.lifecycle.LifecycleObserver);
- method @MainThread public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public androidx.lifecycle.Lifecycle.State currentState;
+ property public int observerCount;
+ field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+ }
+
+ public static final class LifecycleRegistry.Companion {
+ method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
}
@Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt
index 5bbe95f..c7da237 100644
--- a/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime/api/public_plus_experimental_current.txt
@@ -2,15 +2,22 @@
package androidx.lifecycle {
public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
- ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
- method public void addObserver(androidx.lifecycle.LifecycleObserver);
- method @VisibleForTesting public static androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner);
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
method public androidx.lifecycle.Lifecycle.State getCurrentState();
method public int getObserverCount();
- method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
- method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State);
- method public void removeObserver(androidx.lifecycle.LifecycleObserver);
- method @MainThread public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public androidx.lifecycle.Lifecycle.State currentState;
+ property public int observerCount;
+ field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+ }
+
+ public static final class LifecycleRegistry.Companion {
+ method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
}
@Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/api/restricted_current.txt b/lifecycle/lifecycle-runtime/api/restricted_current.txt
index 74af488..4602a21 100644
--- a/lifecycle/lifecycle-runtime/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime/api/restricted_current.txt
@@ -2,15 +2,22 @@
package androidx.lifecycle {
public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
- ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
- method public void addObserver(androidx.lifecycle.LifecycleObserver);
- method @VisibleForTesting public static androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner);
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner provider);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver observer);
+ method @VisibleForTesting public static final androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
method public androidx.lifecycle.Lifecycle.State getCurrentState();
method public int getObserverCount();
- method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
- method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State);
- method public void removeObserver(androidx.lifecycle.LifecycleObserver);
- method @MainThread public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event event);
+ method @Deprecated @MainThread public void markState(androidx.lifecycle.Lifecycle.State state);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver observer);
+ method public void setCurrentState(androidx.lifecycle.Lifecycle.State);
+ property public androidx.lifecycle.Lifecycle.State currentState;
+ property public int observerCount;
+ field public static final androidx.lifecycle.LifecycleRegistry.Companion Companion;
+ }
+
+ public static final class LifecycleRegistry.Companion {
+ method @VisibleForTesting public androidx.lifecycle.LifecycleRegistry createUnsafe(androidx.lifecycle.LifecycleOwner owner);
}
@Deprecated public interface LifecycleRegistryOwner extends androidx.lifecycle.LifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index bd48528..c4f8f49 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -3,6 +3,7 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("org.jetbrains.kotlin.android")
}
android {
@@ -13,12 +14,13 @@
}
dependencies {
+ api(libs.kotlinStdlib)
api(project(":lifecycle:lifecycle-common"))
- api("androidx.arch.core:core-common:2.1.0")
+ api(projectOrArtifact(":arch:core:core-common"))
// necessary for IJ to resolve dependencies.
api("androidx.annotation:annotation:1.1.0")
- implementation("androidx.arch.core:core-runtime:2.1.0")
+ implementation(projectOrArtifact(":arch:core:core-runtime"))
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
deleted file mode 100644
index 5279d4c..0000000
--- a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.DESTROYED;
-import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
-
-import android.annotation.SuppressLint;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.internal.FastSafeIterableMap;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * An implementation of {@link Lifecycle} that can handle multiple observers.
- * <p>
- * It is used by Fragments and Support Library Activities. You can also directly use it if you have
- * a custom LifecycleOwner.
- */
-public class LifecycleRegistry extends Lifecycle {
-
- /**
- * Custom list that keeps observers and can handle removals / additions during traversal.
- *
- * Invariant: at any moment of time for observer1 & observer2:
- * if addition_order(observer1) < addition_order(observer2), then
- * state(observer1) >= state(observer2),
- */
- private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
- new FastSafeIterableMap<>();
- /**
- * Current state
- */
- private State mState;
- /**
- * The provider that owns this Lifecycle.
- * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
- * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
- * because it keeps strong references on all other listeners, so you'll leak all of them as
- * well.
- */
- private final WeakReference<LifecycleOwner> mLifecycleOwner;
-
- private int mAddingObserverCounter = 0;
-
- private boolean mHandlingEvent = false;
- private boolean mNewEventOccurred = false;
-
- // we have to keep it for cases:
- // void onStart() {
- // mRegistry.removeObserver(this);
- // mRegistry.add(newObserver);
- // }
- // newObserver should be brought only to CREATED state during the execution of
- // this onStart method. our invariant with mObserverMap doesn't help, because parent observer
- // is no longer in the map.
- private ArrayList<State> mParentStates = new ArrayList<>();
- private final boolean mEnforceMainThread;
-
- /**
- * Creates a new LifecycleRegistry for the given provider.
- * <p>
- * You should usually create this inside your LifecycleOwner class's constructor and hold
- * onto the same instance.
- *
- * @param provider The owner LifecycleOwner
- */
- public LifecycleRegistry(@NonNull LifecycleOwner provider) {
- this(provider, true);
- }
-
- private LifecycleRegistry(@NonNull LifecycleOwner provider, boolean enforceMainThread) {
- mLifecycleOwner = new WeakReference<>(provider);
- mState = INITIALIZED;
- mEnforceMainThread = enforceMainThread;
- }
-
- /**
- * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
- *
- * @param state new state
- * @deprecated Use {@link #setCurrentState(State)}.
- */
- @Deprecated
- @MainThread
- public void markState(@NonNull State state) {
- enforceMainThreadIfNeeded("markState");
- setCurrentState(state);
- }
-
- /**
- * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
- *
- * @param state new state
- */
- @MainThread
- public void setCurrentState(@NonNull State state) {
- enforceMainThreadIfNeeded("setCurrentState");
- moveToState(state);
- }
-
- /**
- * Sets the current state and notifies the observers.
- * <p>
- * Note that if the {@code currentState} is the same state as the last call to this method,
- * calling this method has no effect.
- *
- * @param event The event that was received
- */
- public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
- enforceMainThreadIfNeeded("handleLifecycleEvent");
- moveToState(event.getTargetState());
- }
-
- private void moveToState(State next) {
- if (mState == next) {
- return;
- }
- if (mState == INITIALIZED && next == DESTROYED) {
- throw new IllegalStateException(
- "no event down from " + mState + " in component " + mLifecycleOwner.get());
- }
- mState = next;
- if (mHandlingEvent || mAddingObserverCounter != 0) {
- mNewEventOccurred = true;
- // we will figure out what to do on upper level.
- return;
- }
- mHandlingEvent = true;
- sync();
- mHandlingEvent = false;
- if (mState == DESTROYED) {
- mObserverMap = new FastSafeIterableMap<>();
- }
- }
-
- private boolean isSynced() {
- if (mObserverMap.size() == 0) {
- return true;
- }
- State eldestObserverState = mObserverMap.eldest().getValue().mState;
- State newestObserverState = mObserverMap.newest().getValue().mState;
- return eldestObserverState == newestObserverState && mState == newestObserverState;
- }
-
- private State calculateTargetState(LifecycleObserver observer) {
- Map.Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);
-
- State siblingState = previous != null ? previous.getValue().mState : null;
- State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
- : null;
- return min(min(mState, siblingState), parentState);
- }
-
- @Override
- public void addObserver(@NonNull LifecycleObserver observer) {
- enforceMainThreadIfNeeded("addObserver");
- State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
- ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
- ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
-
- if (previous != null) {
- return;
- }
- LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
- if (lifecycleOwner == null) {
- // it is null we should be destroyed. Fallback quickly
- return;
- }
-
- boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
- State targetState = calculateTargetState(observer);
- mAddingObserverCounter++;
- while ((statefulObserver.mState.compareTo(targetState) < 0
- && mObserverMap.contains(observer))) {
- pushParentState(statefulObserver.mState);
- final Event event = Event.upFrom(statefulObserver.mState);
- if (event == null) {
- throw new IllegalStateException("no event up from " + statefulObserver.mState);
- }
- statefulObserver.dispatchEvent(lifecycleOwner, event);
- popParentState();
- // mState / subling may have been changed recalculate
- targetState = calculateTargetState(observer);
- }
-
- if (!isReentrance) {
- // we do sync only on the top level.
- sync();
- }
- mAddingObserverCounter--;
- }
-
- private void popParentState() {
- mParentStates.remove(mParentStates.size() - 1);
- }
-
- private void pushParentState(State state) {
- mParentStates.add(state);
- }
-
- @Override
- public void removeObserver(@NonNull LifecycleObserver observer) {
- enforceMainThreadIfNeeded("removeObserver");
- // we consciously decided not to send destruction events here in opposition to addObserver.
- // Our reasons for that:
- // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
- // actually occurred but earlier.
- // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
- // event. If removeObserver method sends destruction events, then a clean up routine becomes
- // more cumbersome. More specific example of that is: your LifecycleObserver listens for
- // a web connection, in the usual routine in OnStop method you report to a server that a
- // session has just ended and you close the connection. Now let's assume now that you
- // lost an internet and as a result you removed this observer. If you get destruction
- // events in removeObserver, you should have a special case in your onStop method that
- // checks if your web connection died and you shouldn't try to report anything to a server.
- mObserverMap.remove(observer);
- }
-
- /**
- * The number of observers.
- *
- * @return The number of observers.
- */
- @SuppressWarnings("WeakerAccess")
- public int getObserverCount() {
- enforceMainThreadIfNeeded("getObserverCount");
- return mObserverMap.size();
- }
-
- @NonNull
- @Override
- public State getCurrentState() {
- return mState;
- }
-
- private void forwardPass(LifecycleOwner lifecycleOwner) {
- Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> ascendingIterator =
- mObserverMap.iteratorWithAdditions();
- while (ascendingIterator.hasNext() && !mNewEventOccurred) {
- Map.Entry<LifecycleObserver, ObserverWithState> entry = ascendingIterator.next();
- ObserverWithState observer = entry.getValue();
- while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred
- && mObserverMap.contains(entry.getKey()))) {
- pushParentState(observer.mState);
- final Event event = Event.upFrom(observer.mState);
- if (event == null) {
- throw new IllegalStateException("no event up from " + observer.mState);
- }
- observer.dispatchEvent(lifecycleOwner, event);
- popParentState();
- }
- }
- }
-
- private void backwardPass(LifecycleOwner lifecycleOwner) {
- Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> descendingIterator =
- mObserverMap.descendingIterator();
- while (descendingIterator.hasNext() && !mNewEventOccurred) {
- Map.Entry<LifecycleObserver, ObserverWithState> entry = descendingIterator.next();
- ObserverWithState observer = entry.getValue();
- while ((observer.mState.compareTo(mState) > 0 && !mNewEventOccurred
- && mObserverMap.contains(entry.getKey()))) {
- Event event = Event.downFrom(observer.mState);
- if (event == null) {
- throw new IllegalStateException("no event down from " + observer.mState);
- }
- pushParentState(event.getTargetState());
- observer.dispatchEvent(lifecycleOwner, event);
- popParentState();
- }
- }
- }
-
- // happens only on the top of stack (never in reentrance),
- // so it doesn't have to take in account parents
- private void sync() {
- LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
- if (lifecycleOwner == null) {
- throw new IllegalStateException("LifecycleOwner of this LifecycleRegistry is already"
- + "garbage collected. It is too late to change lifecycle state.");
- }
- while (!isSynced()) {
- mNewEventOccurred = false;
- // no need to check eldest for nullability, because isSynced does it for us.
- if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
- backwardPass(lifecycleOwner);
- }
- Map.Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
- if (!mNewEventOccurred && newest != null
- && mState.compareTo(newest.getValue().mState) > 0) {
- forwardPass(lifecycleOwner);
- }
- }
- mNewEventOccurred = false;
- }
-
- @SuppressLint("RestrictedApi")
- private void enforceMainThreadIfNeeded(String methodName) {
- if (mEnforceMainThread) {
- if (!ArchTaskExecutor.getInstance().isMainThread()) {
- throw new IllegalStateException("Method " + methodName + " must be called on the "
- + "main thread");
- }
- }
- }
-
- /**
- * Creates a new LifecycleRegistry for the given provider, that doesn't check
- * that its methods are called on the threads other than main.
- * <p>
- * LifecycleRegistry is not synchronized: if multiple threads access this {@code
- * LifecycleRegistry}, it must be synchronized externally.
- * <p>
- * Another possible use-case for this method is JVM testing, when main thread is not present.
- */
- @VisibleForTesting
- @NonNull
- public static LifecycleRegistry createUnsafe(@NonNull LifecycleOwner owner) {
- return new LifecycleRegistry(owner, false);
- }
-
- static State min(@NonNull State state1, @Nullable State state2) {
- return state2 != null && state2.compareTo(state1) < 0 ? state2 : state1;
- }
-
- static class ObserverWithState {
- State mState;
- LifecycleEventObserver mLifecycleObserver;
-
- ObserverWithState(LifecycleObserver observer, State initialState) {
- mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
- mState = initialState;
- }
-
- void dispatchEvent(LifecycleOwner owner, Event event) {
- State newState = event.getTargetState();
- mState = min(mState, newState);
- mLifecycleObserver.onStateChanged(owner, event);
- mState = newState;
- }
- }
-}
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.kt b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.kt
new file mode 100644
index 0000000..8da7e1e
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.kt
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import android.annotation.SuppressLint
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.internal.FastSafeIterableMap
+import java.lang.ref.WeakReference
+
+/**
+ * An implementation of [Lifecycle] that can handle multiple observers.
+ *
+ * It is used by Fragments and Support Library Activities. You can also directly use it if you have
+ * a custom LifecycleOwner.
+ */
+open class LifecycleRegistry private constructor(
+ provider: LifecycleOwner,
+ private val enforceMainThread: Boolean
+) : Lifecycle() {
+ /**
+ * Custom list that keeps observers and can handle removals / additions during traversal.
+ *
+ * Invariant: at any moment of time for observer1 & observer2:
+ * if addition_order(observer1) < addition_order(observer2), then
+ * state(observer1) >= state(observer2),
+ */
+ private var observerMap = FastSafeIterableMap<LifecycleObserver, ObserverWithState>()
+
+ /**
+ * Current state
+ */
+ private var state: State = State.INITIALIZED
+
+ /**
+ * The provider that owns this Lifecycle.
+ * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
+ * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
+ * because it keeps strong references on all other listeners, so you'll leak all of them as
+ * well.
+ */
+ private val lifecycleOwner: WeakReference<LifecycleOwner>
+ private var addingObserverCounter = 0
+ private var handlingEvent = false
+ private var newEventOccurred = false
+
+ // we have to keep it for cases:
+ // void onStart() {
+ // mRegistry.removeObserver(this);
+ // mRegistry.add(newObserver);
+ // }
+ // newObserver should be brought only to CREATED state during the execution of
+ // this onStart method. our invariant with observerMap doesn't help, because parent observer
+ // is no longer in the map.
+ private var parentStates = ArrayList<State>()
+
+ /**
+ * Creates a new LifecycleRegistry for the given provider.
+ *
+ * You should usually create this inside your LifecycleOwner class's constructor and hold
+ * onto the same instance.
+ *
+ * @param provider The owner LifecycleOwner
+ */
+ constructor(provider: LifecycleOwner) : this(provider, true)
+
+ init {
+ lifecycleOwner = WeakReference(provider)
+ }
+
+ /**
+ * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
+ *
+ * @param state new state
+ */
+ @MainThread
+ @Deprecated("Override [currentState].")
+ open fun markState(state: State) {
+ enforceMainThreadIfNeeded("markState")
+ currentState = state
+ }
+
+ override var currentState: State
+ get() = state
+ /**
+ * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
+ *
+ * @param state new state
+ */
+ set(state) {
+ enforceMainThreadIfNeeded("setCurrentState")
+ moveToState(state)
+ }
+
+ /**
+ * Sets the current state and notifies the observers.
+ *
+ * Note that if the `currentState` is the same state as the last call to this method,
+ * calling this method has no effect.
+ *
+ * @param event The event that was received
+ */
+ open fun handleLifecycleEvent(event: Event) {
+ enforceMainThreadIfNeeded("handleLifecycleEvent")
+ moveToState(event.targetState)
+ }
+
+ private fun moveToState(next: State) {
+ if (state == next) {
+ return
+ }
+ check(!(state == State.INITIALIZED && next == State.DESTROYED)) {
+ "no event down from $state in component ${lifecycleOwner.get()}"
+ }
+ state = next
+ if (handlingEvent || addingObserverCounter != 0) {
+ newEventOccurred = true
+ // we will figure out what to do on upper level.
+ return
+ }
+ handlingEvent = true
+ sync()
+ handlingEvent = false
+ if (state == State.DESTROYED) {
+ observerMap = FastSafeIterableMap()
+ }
+ }
+
+ private val isSynced: Boolean
+ get() {
+ if (observerMap.size() == 0) {
+ return true
+ }
+ val eldestObserverState = observerMap.eldest()!!.value.state
+ val newestObserverState = observerMap.newest()!!.value.state
+ return eldestObserverState == newestObserverState && state == newestObserverState
+ }
+
+ private fun calculateTargetState(observer: LifecycleObserver): State {
+ val map = observerMap.ceil(observer)
+ val siblingState = map?.value?.state
+ val parentState =
+ if (parentStates.isNotEmpty()) parentStates[parentStates.size - 1] else null
+ return min(min(state, siblingState), parentState)
+ }
+
+ /**
+ * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
+ * state.
+ *
+ * The given observer will be brought to the current state of the LifecycleOwner.
+ * For example, if the LifecycleOwner is in [Lifecycle.State.STARTED] state, the given observer
+ * will receive [Lifecycle.Event.ON_CREATE], [Lifecycle.Event.ON_START] events.
+ *
+ * @param observer The observer to notify.
+ *
+ * @throws IllegalStateException if no event up from observer's initial state
+ */
+ override fun addObserver(observer: LifecycleObserver) {
+ enforceMainThreadIfNeeded("addObserver")
+ val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
+ val statefulObserver = ObserverWithState(observer, initialState)
+ val previous = observerMap.putIfAbsent(observer, statefulObserver)
+ if (previous != null) {
+ return
+ }
+ val lifecycleOwner = lifecycleOwner.get()
+ ?: // it is null we should be destroyed. Fallback quickly
+ return
+ val isReentrance = addingObserverCounter != 0 || handlingEvent
+ var targetState = calculateTargetState(observer)
+ addingObserverCounter++
+ while (statefulObserver.state < targetState && observerMap.contains(observer)
+ ) {
+ pushParentState(statefulObserver.state)
+ val event = Event.upFrom(statefulObserver.state)
+ ?: throw IllegalStateException("no event up from ${statefulObserver.state}")
+ statefulObserver.dispatchEvent(lifecycleOwner, event)
+ popParentState()
+ // mState / subling may have been changed recalculate
+ targetState = calculateTargetState(observer)
+ }
+ if (!isReentrance) {
+ // we do sync only on the top level.
+ sync()
+ }
+ addingObserverCounter--
+ }
+
+ private fun popParentState() {
+ parentStates.removeAt(parentStates.size - 1)
+ }
+
+ private fun pushParentState(state: State) {
+ parentStates.add(state)
+ }
+
+ override fun removeObserver(observer: LifecycleObserver) {
+ enforceMainThreadIfNeeded("removeObserver")
+ // we consciously decided not to send destruction events here in opposition to addObserver.
+ // Our reasons for that:
+ // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
+ // actually occurred but earlier.
+ // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
+ // event. If removeObserver method sends destruction events, then a clean up routine becomes
+ // more cumbersome. More specific example of that is: your LifecycleObserver listens for
+ // a web connection, in the usual routine in OnStop method you report to a server that a
+ // session has just ended and you close the connection. Now let's assume now that you
+ // lost an internet and as a result you removed this observer. If you get destruction
+ // events in removeObserver, you should have a special case in your onStop method that
+ // checks if your web connection died and you shouldn't try to report anything to a server.
+ observerMap.remove(observer)
+ }
+
+ /**
+ * The number of observers.
+ *
+ * @return The number of observers.
+ */
+ open val observerCount: Int
+ get() {
+ enforceMainThreadIfNeeded("getObserverCount")
+ return observerMap.size()
+ }
+
+ private fun forwardPass(lifecycleOwner: LifecycleOwner) {
+ @Suppress()
+ val ascendingIterator: Iterator<Map.Entry<LifecycleObserver, ObserverWithState>> =
+ observerMap.iteratorWithAdditions()
+ while (ascendingIterator.hasNext() && !newEventOccurred) {
+ val (key, observer) = ascendingIterator.next()
+ while (observer.state < state && !newEventOccurred && observerMap.contains(key)
+ ) {
+ pushParentState(observer.state)
+ val event = Event.upFrom(observer.state)
+ ?: throw IllegalStateException("no event up from ${observer.state}")
+ observer.dispatchEvent(lifecycleOwner, event)
+ popParentState()
+ }
+ }
+ }
+
+ private fun backwardPass(lifecycleOwner: LifecycleOwner) {
+ val descendingIterator = observerMap.descendingIterator()
+ while (descendingIterator.hasNext() && !newEventOccurred) {
+ val (key, observer) = descendingIterator.next()
+ while (observer.state > state && !newEventOccurred && observerMap.contains(key)
+ ) {
+ val event = Event.downFrom(observer.state)
+ ?: throw IllegalStateException("no event down from ${observer.state}")
+ pushParentState(event.targetState)
+ observer.dispatchEvent(lifecycleOwner, event)
+ popParentState()
+ }
+ }
+ }
+
+ // happens only on the top of stack (never in reentrance),
+ // so it doesn't have to take in account parents
+ private fun sync() {
+ val lifecycleOwner = lifecycleOwner.get()
+ ?: throw IllegalStateException(
+ "LifecycleOwner of this LifecycleRegistry is already " +
+ "garbage collected. It is too late to change lifecycle state."
+ )
+ while (!isSynced) {
+ newEventOccurred = false
+ if (state < observerMap.eldest()!!.value.state) {
+ backwardPass(lifecycleOwner)
+ }
+ val newest = observerMap.newest()
+ if (!newEventOccurred && newest != null && state > newest.value.state) {
+ forwardPass(lifecycleOwner)
+ }
+ }
+ newEventOccurred = false
+ }
+
+ @SuppressLint("RestrictedApi")
+ private fun enforceMainThreadIfNeeded(methodName: String) {
+ if (enforceMainThread) {
+ check(ArchTaskExecutor.getInstance().isMainThread) {
+ ("Method $methodName must be called on the main thread")
+ }
+ }
+ }
+
+ internal class ObserverWithState(observer: LifecycleObserver?, initialState: State) {
+ var state: State
+ var lifecycleObserver: LifecycleEventObserver
+
+ init {
+ lifecycleObserver = Lifecycling.lifecycleEventObserver(observer!!)
+ state = initialState
+ }
+
+ fun dispatchEvent(owner: LifecycleOwner?, event: Event) {
+ val newState = event.targetState
+ state = min(state, newState)
+ lifecycleObserver.onStateChanged(owner!!, event)
+ state = newState
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a new LifecycleRegistry for the given provider, that doesn't check
+ * that its methods are called on the threads other than main.
+ *
+ * LifecycleRegistry is not synchronized: if multiple threads access this `LifecycleRegistry`, it must be synchronized externally.
+ *
+ * Another possible use-case for this method is JVM testing, when main thread is not present.
+ */
+ @JvmStatic
+ @VisibleForTesting
+ fun createUnsafe(owner: LifecycleOwner): LifecycleRegistry {
+ return LifecycleRegistry(owner, false)
+ }
+
+ @JvmStatic
+ internal fun min(state1: State, state2: State?): State {
+ return if ((state2 != null) && (state2 < state1)) state2 else state1
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle
index 21eb489..c52cb40 100644
--- a/lifecycle/lifecycle-viewmodel/build.gradle
+++ b/lifecycle/lifecycle-viewmodel/build.gradle
@@ -59,11 +59,3 @@
inceptionYear = "2017"
description = "Android Lifecycle ViewModel"
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
index ebcb9f9..1136d96 100644
--- a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
@@ -28,6 +28,7 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.isKotlin
+import com.android.tools.lint.model.LintModelMavenName
import com.intellij.psi.PsiJvmModifiersOwner
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UMethod
@@ -48,8 +49,16 @@
return InterfaceChecker(context)
}
+ fun LintModelMavenName.asProjectString() = "$groupId.$artifactId"
+
private inner class InterfaceChecker(val context: JavaContext) : UElementHandler() {
override fun visitClass(node: UClass) {
+ // Don't run lint on the set of projects that already used `-Xjvm-default=all` before
+ // all projects were switched over.
+ if (alreadyDefaultAll.contains(context.project.mavenCoordinate?.asProjectString())) {
+ return
+ }
+
if (!isKotlin(node)) return
if (!node.isInterface) return
if (node.annotatedWithAnyOf(
@@ -148,5 +157,76 @@
)
const val JVM_DEFAULT_WITH_COMPATIBILITY = "kotlin.jvm.JvmDefaultWithCompatibility"
+
+ // This set of projects was created by running `grep "Xjvm-default=all" . -r` in the
+ // `frameworks/support` directory and converting the `build.gradle` files in that list to
+ // this format.
+ private val alreadyDefaultAll = setOf(
+ "androidx.room.room-compiler-processing",
+ "androidx.room.room-migration",
+ "androidx.room.room-testing",
+ "androidx.room.room-compiler",
+ "androidx.room.room-ktx",
+ "androidx.room.room-common",
+ "androidx.room.room-runtime",
+ "androidx.compose.ui.ui",
+ "androidx.compose.ui.ui-unit",
+ "androidx.compose.ui.ui-tooling-preview",
+ "androidx.compose.ui.ui-tooling-data",
+ "androidx.compose.ui.ui-util",
+ "androidx.compose.ui.ui-test",
+ "androidx.compose.ui.ui-test-manifest",
+ "androidx.compose.ui.ui-inspection",
+ "androidx.compose.ui.ui-viewbinding",
+ "androidx.compose.ui.ui-geometry",
+ "androidx.compose.ui.ui-graphics",
+ "androidx.compose.ui.ui-text",
+ "androidx.compose.ui.ui-text-google-fonts",
+ "androidx.compose.ui.ui-test-junit4",
+ "androidx.compose.ui.ui-tooling",
+ "androidx.compose.test-utils",
+ "androidx.compose.runtime.runtime",
+ "androidx.compose.runtime.runtime-livedata",
+ "androidx.compose.runtime.runtime-saveable",
+ "androidx.compose.runtime.runtime-rxjava2",
+ "androidx.compose.runtime.runtime-tracing",
+ "androidx.compose.runtime.runtime-rxjava3",
+ "androidx.compose.animation.animation-tooling-internal",
+ "androidx.compose.animation.animation",
+ "androidx.compose.animation.animation-graphics",
+ "androidx.compose.animation.animation-core",
+ "androidx.compose.foundation.foundation",
+ "androidx.compose.foundation.foundation-layout",
+ "androidx.compose.material3.material3-window-size-class",
+ "androidx.compose.material3.material3.integration-tests.material3-catalog",
+ "androidx.compose.material3.material3",
+ "androidx.compose.material.material-ripple",
+ "androidx.lifecycle.lifecycle-viewmodel",
+ "androidx.sqlite.sqlite-ktx",
+ "androidx.sqlite.sqlite-framework",
+ "androidx.sqlite.integration-tests.inspection-sqldelight-testapp",
+ "androidx.sqlite.integration-tests.inspection-room-testapp",
+ "androidx.sqlite.sqlite",
+ "androidx.sqlite.sqlite-inspection",
+ "androidx.tv.tv-foundation",
+ "androidx.tv.tv-material",
+ "androidx.window.window",
+ "androidx.credentials.credentials",
+ "androidx.wear.compose.compose-material",
+ "androidx.wear.watchface.watchface-complications-data-source",
+ "androidx.wear.watchface.watchface",
+ "androidx.wear.watchface.watchface-client",
+ "androidx.lifecycle.lifecycle-common",
+ // These projects didn't already have "Xjvm-default=al", but the only have the error in
+ // integration tests, where the annotation isn't needed.
+ "androidx.annotation.annotation-experimental-lint-integration-tests",
+ "androidx.annotation.annotation-experimental-lint",
+ "androidx.camera.integration-tests.camera-testapp-camera2-pipe",
+ "androidx.compose.integration-tests.docs-snippets",
+ // These projects are excluded due to b/259578592
+ "androidx.camera.camera-camera2-pipe",
+ "androidx.camera.camera-camera2-pipe-integration",
+ "androidx.camera.camera-camera2-pipe-testing",
+ )
}
}
\ No newline at end of file
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index c4e41b3..52ae967 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -56,9 +56,11 @@
import android.view.KeyEvent;
import android.view.Surface;
+import androidx.annotation.DoNotInline;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.concurrent.futures.AbstractResolvableFuture;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.ObjectsCompat;
@@ -203,8 +205,12 @@
mBroadcastReceiver = new MediaButtonReceiver();
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
filter.addDataScheme(mSessionUri.getScheme());
- // TODO(b/197817693): Explicitly indicate whether the receiver should be exported.
- context.registerReceiver(mBroadcastReceiver, filter);
+ if (Build.VERSION.SDK_INT < 33) {
+ context.registerReceiver(mBroadcastReceiver, filter);
+ } else {
+ Api33.registerReceiver(context, mBroadcastReceiver, filter,
+ Context.RECEIVER_NOT_EXPORTED);
+ }
} else {
// Has MediaSessionService to revive playback after it's dead.
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, mSessionUri);
@@ -1690,4 +1696,13 @@
getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
}
};
+
+ @RequiresApi(33)
+ private static class Api33 {
+ @DoNotInline
+ static void registerReceiver(@NonNull Context context, @NonNull BroadcastReceiver receiver,
+ @NonNull IntentFilter filter, int flags) {
+ context.registerReceiver(receiver, filter, flags);
+ }
+ }
}
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 8bfb087..742ab63 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -191,7 +191,7 @@
val nowTime = System.nanoTime()
markStateForRemoval(key, states, nowTime)
states.add(
- StateData(
+ getStateData(
nowTime, -1,
StateInfo(key, value)
)
diff --git a/navigation/navigation-benchmark/lint-baseline.xml b/navigation/navigation-benchmark/lint-baseline.xml
new file mode 100644
index 0000000..a59f98e
--- /dev/null
+++ b/navigation/navigation-benchmark/lint-baseline.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-alpha05" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha05)" variant="all" version="8.0.0-alpha05">
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun navGraphDestinations_withRoutes() = inflateNavGraph_withRoutes(1)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun navGraphDestinations_withRoutes10() = inflateNavGraph_withRoutes(10)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun navGraphDestinations_withRoutes50() = inflateNavGraph_withRoutes(50)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+ </issue>
+
+ <issue
+ id="MissingTestSizeAnnotation"
+ message="Missing test size annotation"
+ errorLine1=" fun navGraphDestinations_withRoutes100() = inflateNavGraph_withRoutes(100)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt"/>
+ </issue>
+
+</issues>
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 4496d11..9e03597 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -141,6 +141,11 @@
enum_constant public static final androidx.paging.LoadType REFRESH;
}
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+ method public boolean isLoggable(int level);
+ method public void log(int level, String message, optional Throwable? tr);
+ }
+
@Deprecated public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor @Deprecated public PageKeyedDataSource();
method @Deprecated public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 20d1eb3..e2b9e00 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -144,6 +144,11 @@
enum_constant public static final androidx.paging.LoadType REFRESH;
}
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+ method public boolean isLoggable(int level);
+ method public void log(int level, String message, optional Throwable? tr);
+ }
+
@Deprecated public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor @Deprecated public PageKeyedDataSource();
method @Deprecated public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 4496d11..9e03597 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -141,6 +141,11 @@
enum_constant public static final androidx.paging.LoadType REFRESH;
}
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+ method public boolean isLoggable(int level);
+ method public void log(int level, String message, optional Throwable? tr);
+ }
+
@Deprecated public abstract class PageKeyedDataSource<Key, Value> extends androidx.paging.DataSource<Key,Value> {
ctor @Deprecated public PageKeyedDataSource();
method @Deprecated public abstract void loadAfter(androidx.paging.PageKeyedDataSource.LoadParams<Key> params, androidx.paging.PageKeyedDataSource.LoadCallback<Key,Value> callback);
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt b/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt
index 38cf905..76de299 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/Logger.kt
@@ -28,6 +28,7 @@
public const val LOG_TAG: String = "Paging"
+@JvmDefaultWithCompatibility
/**
* @hide
*/
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -2,10 +2,12 @@
package androidx.paging.testing {
public final class PagerFlowSnapshotKt {
- method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+ method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
}
public final class SnapshotLoader<Value> {
+ method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -2,10 +2,12 @@
package androidx.paging.testing {
public final class PagerFlowSnapshotKt {
- method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+ method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
}
public final class SnapshotLoader<Value> {
+ method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -2,10 +2,12 @@
package androidx.paging.testing {
public final class PagerFlowSnapshotKt {
- method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+ method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
}
public final class SnapshotLoader<Value> {
+ method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
index abf0652..43eea72 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -17,6 +17,7 @@
package androidx.paging.testing
import androidx.paging.DifferCallback
+import androidx.paging.ItemSnapshotList
import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.NullPaddedList
@@ -33,7 +34,8 @@
import kotlinx.coroutines.withContext
/**
- * Runs the [SnapshotLoader] load operations that are passed in and returns a List of loaded data.
+ * Runs the [SnapshotLoader] load operations that are passed in and returns a List of data
+ * that would be presented to the UI after all load operations are complete.
*
* @param coroutineScope The [CoroutineScope] to collect from this Flow<PagingData> and contains
* the [CoroutineScope.coroutineContext] to load data from.
@@ -42,20 +44,20 @@
*/
public suspend fun <Value : Any> Flow<PagingData<Value>>.asSnapshot(
coroutineScope: CoroutineScope,
- loadOperations: suspend SnapshotLoader<Value>.() -> Unit
-): List<Value> {
+ loadOperations: suspend SnapshotLoader<Value>.() -> @JvmSuppressWildcards Unit
+): @JvmSuppressWildcards List<Value> {
lateinit var loader: SnapshotLoader<Value>
val callback = object : DifferCallback {
override fun onChanged(position: Int, count: Int) {
- loader.onDataSetChanged(loader.generation.value)
+ loader.onDataSetChanged(loader.generations.value)
}
override fun onInserted(position: Int, count: Int) {
- loader.onDataSetChanged(loader.generation.value)
+ loader.onDataSetChanged(loader.generations.value)
}
override fun onRemoved(position: Int, count: Int) {
- loader.onDataSetChanged(loader.generation.value)
+ loader.onDataSetChanged(loader.generations.value)
}
}
@@ -68,6 +70,19 @@
onListPresentable: () -> Unit
): Int? {
onListPresentable()
+ /**
+ * On new generation, SnapshotLoader needs the latest [ItemSnapshotList]
+ * index last updated by the initial refresh so that it can
+ * prepend/append from then on based on that index.
+ *
+ * This last updated index is necessary because initial load
+ * key may not be 0, for example when [Pager].initialKey != 0
+ *
+ * Any subsequent SnapshotLoader loads are based on the index tracked by
+ * [SnapshotLoader] internally.
+ */
+ val lastLoadedIndex = snapshot().placeholdersBefore + snapshot().items.size - 1
+ loader.generations.value.lastAccessedIndex.set(lastLoadedIndex)
return null
}
}
@@ -81,7 +96,7 @@
*/
val job = coroutineScope.launch {
this@asSnapshot.collectLatest {
- // TODO increase generation count
+ incrementGeneration(loader)
differ.collectFrom(it)
}
}
@@ -90,14 +105,16 @@
* Runs the input [loadOperations].
*
* Awaits for initial refresh to complete before invoking [loadOperations]. Automatically
- * cancels the collection on this [Pager.flow] after [loadOperations] completes.
+ * cancels the collection on this [Pager.flow] after [loadOperations] completes and Paging
+ * is idle.
*
* Returns a List of loaded data.
*/
return withContext(coroutineScope.coroutineContext) {
differ.awaitNotLoading()
-
loader.loadOperations()
+ differ.awaitNotLoading()
+
job.cancelAndJoin()
differ.snapshot().items
@@ -118,4 +135,13 @@
private fun LoadStates.isIdle(): Boolean {
return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
prepend is LoadState.NotLoading
+}
+
+private fun <Value : Any> incrementGeneration(loader: SnapshotLoader<Value>) {
+ val currGen = loader.generations.value
+ if (currGen.id == loader.generations.value.id) {
+ loader.generations.value = Generation(
+ id = currGen.id + 1
+ )
+ }
}
\ No newline at end of file
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
index 56cce09..86c1b61 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
@@ -17,8 +17,16 @@
package androidx.paging.testing
import androidx.paging.DifferCallback
+import androidx.paging.PagingData
import androidx.paging.PagingDataDiffer
+import androidx.paging.PagingSource
+import androidx.paging.LoadType.APPEND
+import androidx.paging.PagingConfig
+import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.mapLatest
/**
* Contains the public APIs for load operations in tests.
@@ -29,28 +37,122 @@
public class SnapshotLoader<Value : Any> internal constructor(
private val differ: PagingDataDiffer<Value>
) {
- internal val generation = MutableStateFlow(Generation(0))
+ internal val generations = MutableStateFlow(Generation())
- // TODO add public loading APIs such as scrollTo(index)
+ /**
+ * Refresh the data that is presented on the UI.
+ *
+ * [refresh] triggers a new generation of [PagingData] / [PagingSource]
+ * to represent an updated snapshot of the backing dataset.
+ *
+ * This fake paging operation mimics UI-driven refresh signals such as swipe-to-refresh.
+ */
+ public suspend fun refresh(): @JvmSuppressWildcards Unit {
+ differ.awaitNotLoading()
+ differ.refresh()
+ differ.awaitNotLoading()
+ }
- // the callback to be invoked by DifferCallback on a single generation
- // increase the callbackCount to notify SnapshotLoader that the dataset has updated
+ /**
+ * Imitates scrolling down paged items, [appending][APPEND] data until the given
+ * predicate returns false.
+ *
+ * Note: This API loads an item before passing it into the predicate. This means the
+ * loaded pages may include the page which contains the item that does not match the
+ * predicate. For example, if pageSize = 2, the predicate
+ * {item: Int -> item < 3 } will return items [[1, 2],[3, 4]] where [3, 4] is the page
+ * containing the boundary item[3] not matching the predicate.
+ *
+ * The loaded pages are also dependent on [PagingConfig] settings such as
+ * [PagingConfig.prefetchDistance]:
+ * - if `prefetchDistance` > 0, the resulting appends will include prefetched items.
+ * For example, if pageSize = 2 and prefetchDistance = 2, the predicate
+ * {item: Int -> item < 3 } will load items [[1, 2], [3, 4], [5, 6]] where [5, 6] is the
+ * prefetched page.
+ *
+ * @param [predicate] the predicate to match (return true) to continue append scrolls
+ */
+ public suspend fun appendScrollWhile(
+ predicate: suspend (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+ ): @JvmSuppressWildcards Unit {
+ differ.awaitNotLoading()
+ appendOrPrepend(LoadType.APPEND, predicate)
+ differ.awaitNotLoading()
+ }
+
+ private suspend fun appendOrPrepend(
+ loadType: LoadType,
+ predicate: suspend (item: Value) -> Boolean
+ ) {
+ do {
+ // Get and update the index to load from. Return if index is invalid.
+ val index = nextLoadIndexOrNull(loadType) ?: return
+ val item = loadItem(index)
+ } while (predicate(item))
+ }
+
+ /**
+ * Get and update the index to load from. Returns null if next index is out of bounds.
+ *
+ * This method is responsible for updating the [Generation.lastAccessedIndex] that is then sent
+ * to the differ to trigger load for that index.
+ */
+ private fun nextLoadIndexOrNull(loadType: LoadType): Int? {
+ val currGen = generations.value
+ return when (loadType) {
+ LoadType.PREPEND -> {
+ if (currGen.lastAccessedIndex.get() <= 0) {
+ return null
+ }
+ currGen.lastAccessedIndex.decrementAndGet()
+ }
+ LoadType.APPEND -> {
+ if (currGen.lastAccessedIndex.get() >= differ.size - 1) {
+ return null
+ }
+ currGen.lastAccessedIndex.incrementAndGet()
+ }
+ }
+ }
+
+ // Executes actual loading by accessing the PagingDataDiffer
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ private suspend fun loadItem(index: Int): Value {
+ differ[index]
+
+ // awaits for the item to be loaded
+ return generations.mapLatest {
+ differ.peek(index)
+ }.filterNotNull().first()
+ }
+
+ /**
+ * The callback to be invoked by DifferCallback on a single generation.
+ * Increase the callbackCount to notify SnapshotLoader that the dataset has updated
+ */
internal fun onDataSetChanged(gen: Generation) {
- val currGen = generation.value
+ val currGen = generations.value
// we make sure the generation with the dataset change is still valid because we
// want to disregard callbacks on stale generations
if (gen.id == currGen.id) {
- generation.value = gen.copy(
+ generations.value = gen.copy(
callbackCount = currGen.callbackCount + 1
)
}
}
+
+ private enum class LoadType {
+ PREPEND,
+ APPEND
+ }
}
internal data class Generation(
- // Id of the current Paging generation. Incremented on each new generation (when a new
- // PagingData is received).
- val id: Int,
+ /**
+ * Id of the current Paging generation. Incremented on each new generation (when a new
+ * PagingData is received).
+ */
+ val id: Int = -1,
/**
* A count of the number of times Paging invokes a [DifferCallback] callback within a single
@@ -59,5 +161,10 @@
* The callbackCount enables [SnapshotLoader] to await for a requested item and continue
* loading next item only after a callback is invoked.
*/
- val callbackCount: Int = 0
+ val callbackCount: Int = 0,
+
+ /**
+ * Tracks the last accessed index on the differ for this generation
+ */
+ var lastAccessedIndex: AtomicInteger = AtomicInteger()
)
\ No newline at end of file
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 9304ec8..89a711a 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -18,9 +18,13 @@
import androidx.paging.Pager
import androidx.paging.PagingConfig
+import androidx.paging.cachedIn
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -43,15 +47,29 @@
}
@Test
- fun simpleInitialRefresh() {
+ fun initialRefresh() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {}
+ // first page + prefetched page
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4, 5, 6, 7)
+ )
+ }
+ }
+
+ @Test
+ fun initialRefresh_withoutPrefetch() {
val dataFlow = flowOf(List(30) { it })
val factory = dataFlow.asPagingSourceFactory(testScope)
val pager = Pager(
- config = PagingConfig(
- pageSize = 3,
- initialLoadSize = 5,
- prefetchDistance = 0
- ),
+ config = CONFIG_NO_PREFETCH,
pagingSourceFactory = factory
)
testScope.runTest {
@@ -64,21 +82,408 @@
}
@Test
- fun emptyInitialRefresh() {
- val dataFlow = emptyFlow<List<Int>>()
+ fun initialRefresh_withInitialKey() {
+ val dataFlow = flowOf(List(30) { it })
val factory = dataFlow.asPagingSourceFactory(testScope)
val pager = Pager(
- config = PagingConfig(
- pageSize = 3,
- initialLoadSize = 5,
- prefetchDistance = 0
- ),
+ config = CONFIG,
+ initialKey = 10,
pagingSourceFactory = factory
)
testScope.runTest {
val snapshot = pager.flow.asSnapshot(this) {}
- assertThat(snapshot).isEmpty()
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+ )
}
}
+
+ @Test
+ fun initialRefresh_withInitialKey_withoutPrefetch() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {}
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(10, 11, 12, 13, 14)
+ )
+ }
+ }
+
+ @Test
+ fun emptyInitialRefresh() {
+ val dataFlow = emptyFlow<List<Int>>()
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {}
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+ }
+ }
+
+ @Test
+ fun manualRefresh() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = factory
+ ).flow.cachedIn(testScope.backgroundScope)
+
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ refresh()
+ }
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4),
+ )
+ }
+ }
+
+ @Test
+ fun manualEmptyRefresh() {
+ val dataFlow = emptyFlow<List<Int>>()
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ refresh()
+ }
+ assertThat(snapshot).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+ }
+ }
+
+ @Test
+ fun append() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 7
+ }
+ }
+
+ // includes initial load, 1st page, 2nd page (from prefetch)
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ )
+ }
+ }
+
+ @Test
+ fun append_withInitialKey() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ initialKey = 10,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 18
+ }
+ }
+
+ // items[7-23]
+ // extra prepended page from prefetch after initial refresh
+ // extra appended page from prefetch after append
+ assertThat(snapshot).containsExactlyElementsIn(
+ List(17) { it + 7 }
+ )
+ }
+ }
+
+ @Test
+ fun append_withInitialKey_withoutPrefetch() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 18
+ }
+ }
+
+ // items[10-20]
+ // although no prefetch, extra appended page because paging loaded item 18
+ // and its entire page before the predicate returned false
+ assertThat(snapshot).containsExactlyElementsIn(
+ List(11) { it + 10 }
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveAppend() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 7
+ }
+ }
+
+ val snapshot2 = pager.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 22
+ }
+ }
+
+ // includes initial load, 1st page, 2nd page (from prefetch)
+ assertThat(snapshot1).containsExactlyElementsIn(
+ List(11) { it }
+ )
+
+ // includes extra page from prefetch
+ assertThat(snapshot2).containsExactlyElementsIn(
+ List(26) { it }
+ )
+ }
+ }
+
+ @Test
+ fun append_outOfBounds_returnsCurrentlyLoadedItems() {
+ val dataFlow = flowOf(List(10) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ // condition scrolls till end of data since we only have 10 items
+ item < 18
+ }
+ }
+
+ // returns the items loaded before index becomes out of bounds
+ assertThat(snapshot).containsExactlyElementsIn(
+ List(10) { it }
+ )
+ }
+ }
+
+ @Test
+ fun refreshAndAppend() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ refresh() // triggers second gen
+ appendScrollWhile { item: Int ->
+ item < 10
+ }
+ }
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+ )
+ }
+ }
+
+ @Test
+ fun appendAndRefresh() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 10
+ }
+ refresh()
+ }
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ // second gen initial load, anchorPos = 10, refreshKey = 8
+ listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_fromSharedFlow() {
+ val dataFlow = MutableSharedFlow<List<Int>>()
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) { }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ dataFlow.emit(List(30) { it })
+ }
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4)
+ )
+
+ val snapshot3 = pager.asSnapshot(this) {
+ dataFlow.emit(List(30) { it + 30 })
+ }
+ assertThat(snapshot3).containsExactlyElementsIn(
+ listOf(30, 31, 32, 33, 34)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_fromFlow() {
+ val dataFlow = flow {
+ // first gen
+ emit(emptyList())
+ delay(500)
+ // second gen
+ emit(List(30) { it })
+ delay(500)
+ // third gen
+ emit(List(30) { it + 30 })
+ }
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) { }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ delay(500)
+ }
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4)
+ )
+
+ val snapshot3 = pager.asSnapshot(this) {
+ delay(500)
+ }
+ assertThat(snapshot3).containsExactlyElementsIn(
+ listOf(30, 31, 32, 33, 34)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_withInitialKey_nullRefreshKey() {
+ val dataFlow = flow {
+ // first gen
+ emit(List(20) { it })
+ delay(500)
+ // second gen
+ emit(List(20) { it })
+ }
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) { }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ listOf(10, 11, 12, 13, 14)
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ // wait for second gen to complete
+ delay(500)
+ }
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_withInitialKey_nonNullRefreshKey() {
+ val dataFlow = flow {
+ // first gen
+ emit(List(20) { it })
+ delay(500)
+ // second gen
+ emit(List(20) { it })
+ }
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) {
+ // we scroll to register a non-null anchorPos
+ appendScrollWhile { item: Int ->
+ item < 15
+ }
+ }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ listOf(10, 11, 12, 13, 14, 15, 16, 17)
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ delay(500)
+ }
+ // anchorPos = 15, refreshKey = 13
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(13, 14, 15, 16, 17)
+ )
+ }
+ }
+
+ val CONFIG = PagingConfig(
+ pageSize = 3,
+ initialLoadSize = 5,
+ )
+
+ val CONFIG_NO_PREFETCH = PagingConfig(
+ pageSize = 3,
+ initialLoadSize = 5,
+ prefetchDistance = 0
+ )
}
\ No newline at end of file
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index c032acf..e853b9d 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9282206
-androidx.playground.metalavaBuildId=9295138
+androidx.playground.snapshotBuildId=9304612
+androidx.playground.metalavaBuildId=9297860
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
index f684502..4606a745 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.tools.apicompiler
+import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.apicompiler.generator.SandboxApiVersion
import androidx.privacysandbox.tools.apicompiler.generator.SdkCodeGenerator
import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
@@ -35,16 +36,19 @@
) : SymbolProcessor {
companion object {
const val AIDL_COMPILER_PATH_OPTIONS_KEY = "aidl_compiler_path"
- const val USE_COMPAT_LIBRARY_OPTIONS_KEY = "use_sdk_runtime_compat_library"
+ const val SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY = "skip_sdk_runtime_compat_library"
}
- var invoked = false
-
override fun process(resolver: Resolver): List<KSAnnotated> {
- if (invoked) {
+ // This method is called multiple times during compilation and the resolver will only return
+ // relevant files for each particular processing round. This instance might also be kept
+ // by KSP between rounds or for incremental compilation. This means that at some point
+ // KSP will always invoke this processor with no valid services, so we should just stop
+ // processing.
+ if (resolver.getSymbolsWithAnnotation(
+ PrivacySandboxService::class.qualifiedName!!).none()) {
return emptyList()
}
- invoked = true
val path = options[AIDL_COMPILER_PATH_OPTIONS_KEY]?.let(Paths::get)
if (path == null) {
@@ -52,9 +56,11 @@
return emptyList()
}
- val target = if (options[USE_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase() == "true") {
- SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
- } else SandboxApiVersion.API_33
+ val skipCompatLibrary =
+ options[SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase().toBoolean()
+ val target = if (skipCompatLibrary) {
+ SandboxApiVersion.API_33
+ } else SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
val parsedApi = ApiParser(resolver, logger).parseApi()
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index bc189a4..78e3098 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -75,7 +75,7 @@
// Sources created by the AIDL compiler have to be copied to files created
// through the KSP APIs, so that they are included in downstream compilation.
val kspGeneratedFile = codeGenerator.createNewFile(
- Dependencies(false),
+ Dependencies.ALL_FILES,
source.packageName,
source.interfaceName,
extensionName = "java"
@@ -113,7 +113,7 @@
private fun generateToolMetadata() {
codeGenerator.createNewFile(
- Dependencies(false),
+ Dependencies.ALL_FILES,
Metadata.filePath.parent.toString(),
Metadata.filePath.nameWithoutExtension,
Metadata.filePath.extension,
@@ -138,8 +138,7 @@
}
private fun write(spec: FileSpec) {
-
- codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
+ codeGenerator.createNewFile(Dependencies.ALL_FILES, spec.packageName, spec.name)
.bufferedWriter().use(spec::writeTo)
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 23fb1b7..5782e08 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -39,8 +39,9 @@
/** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
- private val interfaceParser = InterfaceParser(logger)
- private val valueParser = ValueParser(logger)
+ private val typeParser = TypeParser(logger)
+ private val interfaceParser = InterfaceParser(logger, typeParser)
+ private val valueParser = ValueParser(logger, typeParser)
fun parseApi(): ParsedApi {
val services = parseAllServices()
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
deleted file mode 100644
index 2c0f555..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2022 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.tools.apicompiler.parser
-
-import androidx.privacysandbox.tools.core.model.Type
-import com.google.devtools.ksp.symbol.KSDeclaration
-
-internal object Converters {
- fun typeFromDeclaration(declaration: KSDeclaration): Type {
- return Type(
- packageName = declaration.packageName.getFullName(),
- simpleName = declaration.simpleName.getShortName(),
- )
- }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index 79c69c9..cb2b9d0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -19,7 +19,6 @@
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.model.Parameter
-import androidx.privacysandbox.tools.core.model.Type
import com.google.devtools.ksp.getDeclaredFunctions
import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.isPublic
@@ -27,12 +26,10 @@
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
-import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
-internal class InterfaceParser(private val logger: KSPLogger) {
+internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
@@ -47,13 +44,15 @@
}
if (interfaceDeclaration.getDeclaredProperties().any()) {
logger.error(
- "Error in $name: annotated interfaces cannot declare properties.")
+ "Error in $name: annotated interfaces cannot declare properties."
+ )
}
if (interfaceDeclaration.declarations.filterIsInstance<KSClassDeclaration>()
.any(KSClassDeclaration::isCompanionObject)
) {
logger.error(
- "Error in $name: annotated interfaces cannot declare companion objects.")
+ "Error in $name: annotated interfaces cannot declare companion objects."
+ )
}
val invalidModifiers =
interfaceDeclaration.modifiers.filterNot(validInterfaceModifiers::contains)
@@ -75,7 +74,7 @@
val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
return AnnotatedInterface(
- type = Converters.typeFromDeclaration(interfaceDeclaration),
+ type = typeParser.parseFromDeclaration(interfaceDeclaration),
methods = methods,
)
}
@@ -102,7 +101,7 @@
if (method.returnType == null) {
logger.error("Error in $name: failed to resolve return type.")
}
- val returnType = parseType(method, method.returnType!!.resolve())
+ val returnType = typeParser.parseFromTypeReference(method.returnType!!, name)
val parameters =
method.parameters.map { parameter -> parseParameter(method, parameter) }.toList()
@@ -128,15 +127,7 @@
return Parameter(
name = parameter.name!!.getFullName(),
- type = parseType(method, parameter.type.resolve()),
+ type = typeParser.parseFromTypeReference(parameter.type, name),
)
}
-
- private fun parseType(method: KSFunctionDeclaration, type: KSType): Type {
- val name = method.qualifiedName?.getFullName() ?: method.simpleName.getFullName()
- if (type.nullability == Nullability.NULLABLE) {
- logger.error("Error in $name: nullable types are not supported.")
- }
- return Converters.typeFromDeclaration(type.declaration)
- }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
new file mode 100644
index 0000000..38a96b6
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2022 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.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.core.model.Type
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Nullability
+import com.google.devtools.ksp.symbol.Variance
+
+internal class TypeParser(private val logger: KSPLogger) {
+ fun parseFromDeclaration(declaration: KSDeclaration): Type {
+ return Type(
+ packageName = declaration.packageName.getFullName(),
+ simpleName = declaration.simpleName.getShortName(),
+ )
+ }
+
+ fun parseFromTypeReference(typeReference: KSTypeReference, debugName: String): Type {
+ val resolvedType = typeReference.resolve()
+ if (resolvedType.isError) {
+ logger.error("Failed to resolve type for $debugName.")
+ }
+ if (resolvedType.nullability == Nullability.NULLABLE) {
+ logger.error("Error in $debugName: nullable types are not supported.")
+ }
+ val typeArguments = typeReference.element?.typeArguments?.mapNotNull {
+ if (it.type == null) {
+ logger.error("Error in $debugName: null type argument.")
+ }
+ if (it.variance != Variance.INVARIANT) {
+ logger.error("Error in $debugName: only invariant type arguments are supported.")
+ }
+ it.type
+ } ?: emptyList()
+ return Type(
+ packageName = resolvedType.declaration.packageName.getFullName(),
+ simpleName = resolvedType.declaration.simpleName.getShortName(),
+ typeParameters = typeArguments.map { parseFromTypeReference(it, debugName) },
+ )
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
index 39d8a26..64f9c6b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
@@ -25,13 +25,13 @@
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
-internal class ValueParser(private val logger: KSPLogger) {
+internal class ValueParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
fun parseValue(value: KSAnnotated): AnnotatedValue? {
if (value !is KSClassDeclaration ||
value.classKind != ClassKind.CLASS ||
- !value.modifiers.contains(Modifier.DATA)) {
+ !value.modifiers.contains(Modifier.DATA)
+ ) {
logger.error(
"Only data classes can be annotated with @PrivacySandboxValue."
)
@@ -61,7 +61,7 @@
}
return AnnotatedValue(
- type = Converters.typeFromDeclaration(value),
+ type = typeParser.parseFromDeclaration(value),
properties = value.getAllProperties().map(::parseProperty).toList()
)
}
@@ -71,17 +71,9 @@
if (property.isMutable) {
logger.error("Error in $name: properties cannot be mutable.")
}
- val type = property.type.resolve()
- if (type.isError) {
- logger.error("Failed to resolve type for property $name.")
- }
- if (type.nullability == Nullability.NULLABLE) {
- logger.error("Error in $name: nullable types are not supported.")
- }
-
return ValueProperty(
name = property.simpleName.getShortName(),
- type = Converters.typeFromDeclaration(type.declaration),
+ type = typeParser.parseFromTypeReference(property.type, name),
)
}
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
index 78f636c..454d7b8 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
@@ -34,7 +34,11 @@
val inputSources = loadSourcesFromDirectory(inputTestDataDir)
val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
- val result = compileWithPrivacySandboxKspCompiler(inputSources)
+ val result = compileWithPrivacySandboxKspCompiler(
+ inputSources,
+ platformStubs = PlatformStubs.API_33,
+ extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
+ )
assertThat(result).succeeds()
val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
index cda0661..1ef27b0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
@@ -32,11 +32,7 @@
val inputSources = loadSourcesFromDirectory(inputTestDataDir)
val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
- val result = compileWithPrivacySandboxKspCompiler(
- inputSources,
- platformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
- extraProcessorOptions = mapOf("use_sdk_runtime_compat_library" to "true")
- )
+ val result = compileWithPrivacySandboxKspCompiler(inputSources)
assertThat(result).succeeds()
val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index 5de917b..e6b6e3d 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -28,7 +28,7 @@
*/
fun compileWithPrivacySandboxKspCompiler(
sources: List<Source>,
- platformStubs: PlatformStubs = PlatformStubs.API_33,
+ platformStubs: PlatformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
extraProcessorOptions: Map<String, String> = mapOf(),
): TestCompilationResult {
val provider = PrivacySandboxKspCompiler.Provider()
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index afda28e..f37b09bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -43,6 +43,7 @@
@PrivacySandboxService
interface MySdk {
suspend fun doStuff(x: Int, y: Int): String
+ suspend fun processList(list: List<Int>): List<String>
fun doMoreStuff()
}
""",
@@ -66,6 +67,17 @@
),
returnType = Types.string,
isSuspend = true,
+ ),
+ Method(
+ name = "processList",
+ parameters = listOf(
+ Parameter(
+ name = "list",
+ type = Types.list(Types.int),
+ )
+ ),
+ returnType = Types.list(Types.string),
+ isSuspend = true,
), Method(
name = "doMoreStuff",
parameters = listOf(),
@@ -220,9 +232,18 @@
fun parameterWithGenerics_fails() {
checkSourceFails(serviceMethod("suspend fun foo(x: MutableList<Int>)"))
.containsExactlyErrors(
- "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
- "or @PrivacySandboxInterface are supported as parameter types."
+ "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+ "parameter types."
+ )
+ }
+
+ @Test
+ fun listParameterWithNonInvariantTypeArgument_fails() {
+ checkSourceFails(serviceMethod("suspend fun foo(x: List<in Int>)"))
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdk.foo: only invariant type arguments are supported."
)
}
@@ -230,9 +251,10 @@
fun parameterLambda_fails() {
checkSourceFails(serviceMethod("suspend fun foo(x: (Int) -> Int)"))
.containsExactlyErrors(
- "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
- "or @PrivacySandboxInterface are supported as parameter types."
+ "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+ "parameter types."
)
}
@@ -251,7 +273,7 @@
"""
)
checkSourceFails(source).containsExactlyErrors(
- "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
+ "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
"supported as return types."
)
@@ -361,12 +383,14 @@
name = "request",
type = Type(
packageName = "com.mysdk",
- simpleName = "MyInterface"),
+ simpleName = "MyInterface"
+ ),
)
),
returnType = Type(
packageName = "com.mysdk",
- simpleName = "MyInterface"),
+ simpleName = "MyInterface"
+ ),
isSuspend = true,
),
)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index 5dd00bf..132a95a 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -52,7 +52,7 @@
data class MySdkResponse(
val magicPayload: MagicPayload, val isTrulyMagic: Boolean)
@PrivacySandboxValue
- data class MagicPayload(val magicNumber: Long)
+ data class MagicPayload(val magicList: List<Long>)
"""
)
assertThat(parseSources(source)).isEqualTo(
@@ -92,9 +92,7 @@
),
AnnotatedValue(
type = Type(packageName = "com.mysdk", simpleName = "MagicPayload"),
- properties = listOf(
- ValueProperty("magicNumber", Types.long),
- )
+ properties = listOf(ValueProperty("magicList", Types.list(Types.long)))
),
)
)
@@ -178,7 +176,7 @@
)
checkSourceFails(dataClass)
.containsExactlyErrors(
- "Error in com.mysdk.MySdkRequest.foo: only primitives, data classes " +
+ "Error in com.mysdk.MySdkRequest.foo: only primitives, lists, data classes " +
"annotated with @PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as properties."
)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt
index d130eaf..a9fb9fc 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkFactory.kt
@@ -1,7 +1,7 @@
package com.mysdk
import android.os.IBinder
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.`internal`.GeneratedPublicApi
@GeneratedPublicApi
public object MySdkFactory {
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
index 8fa3062..63749650 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkFactory.kt
@@ -1,7 +1,7 @@
package com.mysdk
import android.os.IBinder
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.`internal`.GeneratedPublicApi
@GeneratedPublicApi
public object BackwardsCompatibleSdkFactory {
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 1db70b6..e588bbd 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -109,10 +109,14 @@
if (classifier !is KmClassifier.Class) {
throw PrivacySandboxParsingException("Unsupported type in API description: $type")
}
- return parseClassName(classifier.name)
+ val typeArguments = type.arguments.map { parseType(it.type!!) }
+ return parseClassName(classifier.name, typeArguments)
}
- private fun parseClassName(className: ClassName): Type {
+ private fun parseClassName(
+ className: ClassName,
+ typeArguments: List<Type> = emptyList()
+ ): Type {
// Package names are separated with slashes and nested classes are separated with dots.
// (e.g com/example/OuterClass.InnerClass).
val (packageName, simpleName) = className.split('/').run {
@@ -126,7 +130,7 @@
)
}
- return Type(packageName, simpleName)
+ return Type(packageName, simpleName, typeArguments)
}
private fun validate(api: ParsedApi) {
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index cc006e8..bb3c24f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -49,6 +49,7 @@
fun doSomething(magicNumber: Int, awesomeString: String)
suspend fun getPayload(request: PayloadRequest): PayloadResponse
suspend fun getInterface(): MyInterface
+ suspend fun processList(list: List<Long>): List<Long>
}
@PrivacySandboxInterface
interface MyInterface {
@@ -64,7 +65,7 @@
interface CustomCallback {
fun onComplete(status: Int)
}
- """
+ """,
)
val expectedPayloadType = AnnotatedValue(
@@ -118,7 +119,18 @@
),
returnType = expectedPayloadResponse.type,
isSuspend = true,
- )
+ ),
+ Method(
+ name = "processList",
+ parameters = listOf(
+ Parameter(
+ "list",
+ Types.list(Types.long)
+ )
+ ),
+ returnType = Types.list(Types.long),
+ isSuspend = true,
+ ),
)
)
val expectedInterface =
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
index c7020f3..6b0bafd 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
@@ -20,7 +20,7 @@
import androidx.privacysandbox.tools.PrivacySandboxInterface
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxValue
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.internal.GeneratedPublicApi
import java.nio.file.Path
import kotlin.io.path.readBytes
import org.objectweb.asm.AnnotationVisitor
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index bd3cc87..e537412 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -90,7 +90,7 @@
|import androidx.privacysandbox.tools.PrivacySandboxCallback
|import androidx.privacysandbox.tools.PrivacySandboxService
|import androidx.privacysandbox.tools.PrivacySandboxValue
- |import androidx.privacysandbox.tools.core.GeneratedPublicApi
+ |import androidx.privacysandbox.tools.internal.GeneratedPublicApi
|
|@PrivacySandboxService
|interface MySdk
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index f18b9ba..bb18c7b 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -29,6 +29,7 @@
api(libs.kotlinStdlib)
api(libs.protobufLite)
api(libs.kotlinPoet)
+ implementation project(path: ':privacysandbox:tools:tools')
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt
index 8a68c35..c0fe64f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServiceFactoryFileGenerator.kt
@@ -16,7 +16,7 @@
package androidx.privacysandbox.tools.core.generator
-import androidx.privacysandbox.tools.core.GeneratedPublicApi
+import androidx.privacysandbox.tools.internal.GeneratedPublicApi
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt
index 6d61221..19ea909 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlFileSpec.kt
@@ -18,6 +18,7 @@
import androidx.privacysandbox.tools.core.model.Type
+@JvmDefaultWithCompatibility
/** Describes the contents of an AIDL file. */
internal interface AidlFileSpec {
val type: Type
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index 467d8ac..0ee111d 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -41,4 +41,9 @@
val char = Type(packageName = "kotlin", simpleName = "Char")
val short = Type(packageName = "kotlin", simpleName = "Short")
val primitiveTypes = setOf(unit, boolean, int, long, float, double, string, char, short)
+ fun list(elementType: Type) = Type(
+ packageName = "kotlin.collections",
+ simpleName = "List",
+ typeParameters = listOf(elementType)
+ )
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
index 71e5cd2..3442e0f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
@@ -19,6 +19,7 @@
data class Type(
val packageName: String,
val simpleName: String,
+ val typeParameters: List<Type> = emptyList(),
) {
val qualifiedName = "$packageName.$simpleName"
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index 0f6e3f8..3b5f012 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -19,9 +19,14 @@
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.AnnotatedValue
import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Type
import androidx.privacysandbox.tools.core.model.Types
class ModelValidator private constructor(val api: ParsedApi) {
+ private val values = api.values.map(AnnotatedValue::type)
+ private val interfaces = api.interfaces.map(AnnotatedInterface::type)
+ private val callbacks = api.callbacks.map(AnnotatedInterface::type)
+
private val errors: MutableList<String> = mutableListOf()
companion object {
@@ -61,33 +66,24 @@
}
private fun validateServiceAndInterfaceMethods() {
- val allowedParameterTypes =
- (api.values.map(AnnotatedValue::type) +
- api.callbacks.map(AnnotatedInterface::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
- val allowedReturnValueTypes =
- (api.values.map(AnnotatedValue::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
-
val annotatedInterfaces = api.services + api.interfaces
for (annotatedInterface in annotatedInterfaces) {
for (method in annotatedInterface.methods) {
- if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+ if (method.parameters.any { !(isValidInterfaceParameterType(it.type)) }) {
errors.add(
"Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
- "only primitives, data classes annotated with @PrivacySandboxValue " +
- "and interfaces annotated with @PrivacySandboxCallback or " +
- "@PrivacySandboxInterface are supported as parameter types."
+ "only primitives, lists, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported " +
+ "as parameter types."
)
}
- if (!allowedReturnValueTypes.contains(method.returnType)) {
+ if (!isValidInterfaceReturnType(method.returnType)) {
errors.add(
"Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
- "only primitives, data classes annotated with @PrivacySandboxValue " +
- "and interfaces annotated with @PrivacySandboxInterface are " +
- "supported as return types."
+ "only primitives, lists, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as return types."
)
}
}
@@ -95,17 +91,12 @@
}
private fun validateValuePropertyTypes() {
- val allowedValuePropertyTypes =
- (api.values.map(AnnotatedValue::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
-
for (value in api.values) {
for (property in value.properties) {
- if (!allowedValuePropertyTypes.contains(property.type)) {
+ if (!isValidValuePropertyType(property.type)) {
errors.add(
"Error in ${value.type.qualifiedName}.${property.name}: " +
- "only primitives, data classes annotated with " +
+ "only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as properties."
)
@@ -115,17 +106,12 @@
}
private fun validateCallbackMethods() {
- val allowedParameterTypes =
- (api.values.map(AnnotatedValue::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
-
for (callback in api.callbacks) {
for (method in callback.methods) {
- if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+ if (method.parameters.any { !isValidCallbackParameterType(it.type) }) {
errors.add(
"Error in ${callback.type.qualifiedName}.${method.name}: " +
- "only primitives, data classes annotated with " +
+ "only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as callback parameter types."
)
@@ -139,6 +125,29 @@
}
}
}
+
+ private fun isValidInterfaceParameterType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type) || isCallback(type)
+ private fun isValidInterfaceReturnType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+ private fun isValidValuePropertyType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+ private fun isValidCallbackParameterType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+
+ private fun isValue(type: Type) = values.contains(type)
+ private fun isInterface(type: Type) = interfaces.contains(type)
+ private fun isCallback(type: Type) = callbacks.contains(type)
+ private fun isPrimitive(type: Type) = Types.primitiveTypes.contains(type)
+ private fun isList(type: Type): Boolean {
+ if (type.qualifiedName == "kotlin.collections.List") {
+ require(type.typeParameters.size == 1) {
+ "List type should have one type parameter, found ${type.typeParameters}."
+ }
+ return type.typeParameters[0].let { isValue(it) || isPrimitive(it) }
+ }
+ return false
+ }
}
data class ValidationResult(val errors: List<String>) {
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
index c3a654d..d13fd0e 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
@@ -239,12 +239,45 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.MySdk.returnFoo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
- "supported as return types.",
- "Error in com.mysdk.MySdk.receiveFoo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback or " +
- "@PrivacySandboxInterface are supported as parameter types."
+ "Error in com.mysdk.MySdk.returnFoo: only primitives, lists, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as return types.",
+ "Error in com.mysdk.MySdk.receiveFoo: only primitives, lists, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as parameter " +
+ "types."
+ )
+ }
+
+ @Test
+ fun nestedList_throws() {
+ val api = ParsedApi(
+ services = setOf(
+ AnnotatedInterface(
+ type = Type(packageName = "com.mysdk", simpleName = "MySdk"),
+ methods = listOf(
+ Method(
+ name = "processNestedList",
+ parameters = listOf(
+ Parameter(
+ name = "foo",
+ type = Types.list(Types.list(Types.int))
+ )
+ ),
+ returnType = Types.unit,
+ isSuspend = true,
+ ),
+ ),
+ ),
+ )
+ )
+ val validationResult = ModelValidator.validate(api)
+ assertThat(validationResult.isFailure).isTrue()
+ assertThat(validationResult.errors).containsExactly(
+ "Error in com.mysdk.MySdk.processNestedList: only primitives, lists, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+ "parameter types."
)
}
@@ -266,7 +299,7 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.Foo.bar: only primitives, data classes annotated with " +
+ "Error in com.mysdk.Foo.bar: only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface " +
"are supported as properties."
)
@@ -333,8 +366,8 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.MySdkCallback.foo: only primitives, data classes annotated " +
- "with @PrivacySandboxValue and interfaces annotated with " +
+ "Error in com.mysdk.MySdkCallback.foo: only primitives, lists, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as callback parameter types."
)
}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/GeneratedPublicApi.kt b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/internal/GeneratedPublicApi.kt
similarity index 88%
rename from privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/GeneratedPublicApi.kt
rename to privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/internal/GeneratedPublicApi.kt
index 6a4fef0..410eacb 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/GeneratedPublicApi.kt
+++ b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/internal/GeneratedPublicApi.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.core
+package androidx.privacysandbox.tools.internal
/**
* Indicates that a class was generated by the API Compiler and is part of the public facing
@@ -22,7 +22,7 @@
*
* The API Packager will include these classes in the API descriptors.
*
- * This annotation is for internal usage and will only be set by the API Compiler.
+ * THIS ANNOTATION IS FOR INTERNAL USAGE ONLY.
* The API Compiler will fail if the annotation is present in the source code.
*/
@Retention(AnnotationRetention.BINARY)
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index f00957f..6bf3a91 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -1296,6 +1296,19 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
@Test
+ public void onInitializeAccessibilityNodeInfo_classNameAdded()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
+ final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+ mActivityRule.runOnUiThread(
+ () -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo));
+
+ assertEquals(nodeInfo.getClassName(), "android.widget.ListView");
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
throws Throwable {
setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
index aa53614..7fca165 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
@@ -1427,6 +1427,19 @@
}
@Test
+ public void rowCountForAccessibility_verticalOrientation()
+ throws Throwable {
+ Config config = new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(100);
+ setupByConfig(config);
+ waitFirstLayout();
+
+ int count = mLayoutManager.getRowCountForAccessibility(mRecyclerView.mRecycler,
+ mRecyclerView.mState);
+
+ assertEquals(-1, count);
+ }
+
+ @Test
public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
throws Throwable {
final int itemCount = 2;
@@ -1441,6 +1454,19 @@
}
@Test
+ public void columnCountForAccessibility_horizontalOrientation()
+ throws Throwable {
+ Config config = new Config(HORIZONTAL, false, 3, GAP_HANDLING_NONE).itemCount(100);
+ setupByConfig(config);
+ waitFirstLayout();
+
+ int count = mLayoutManager.getColumnCountForAccessibility(mRecyclerView.mRecycler,
+ mRecyclerView.mState);
+
+ assertEquals(-1, count);
+ }
+
+ @Test
public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
throws Throwable {
final int itemCount = 2;
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index ac79210..da23dcc 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -30,6 +30,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -293,6 +294,10 @@
public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(recycler, state, info);
+ // Set the class name so this is treated as a list. This helps accessibility services
+ // distinguish lists from one row or one column grids.
+ info.setClassName(ListView.class.getName());
+
// TODO(b/251823537)
if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
index df03e317..754d879 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
@@ -1358,7 +1358,7 @@
if (mOrientation == HORIZONTAL) {
return Math.min(mSpanCount, state.getItemCount());
}
- return super.getRowCountForAccessibility(recycler, state);
+ return -1;
}
@Override
@@ -1367,7 +1367,7 @@
if (mOrientation == VERTICAL) {
return Math.min(mSpanCount, state.getItemCount());
}
- return super.getColumnCountForAccessibility(recycler, state);
+ return -1;
}
/**
diff --git a/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt b/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt
index 197a5a1..7bbefa1 100644
--- a/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt
+++ b/resourceinspection/resourceinspection-processor/src/main/kotlin/androidx/resourceinspection/processor/Models.kt
@@ -29,6 +29,7 @@
val className: ClassName = ClassName.get(type)
}
+@JvmDefaultWithCompatibility
internal interface Attribute {
val name: String
val namespace: String
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index 37e8d81..13949d9 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -51,6 +51,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking
+@JvmDefaultWithCompatibility
@Dao
@TypeConverters(DateConverter::class, AnswerConverter::class)
interface BooksDao {
@@ -452,8 +453,9 @@
action: suspend (input: Book) -> Book
): Book = action(input)
+ // Commented out because of https://youtrack.jetbrains.com/issue/KT-48013
// This is a private method to validate b/194706278
- private fun getNullAuthor(): Author? = null
+ // private fun getNullAuthor(): Author? = null
@Query("SELECT * FROM Publisher JOIN Book ON (Publisher.publisherId == Book.bookPublisherId)")
fun getBooksByPublisher(): Map<Publisher, List<Book>>
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
index 8e5ba0c..b50dbce 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
@@ -21,6 +21,7 @@
import androidx.room.Transaction
import androidx.room.integration.kotlintestapp.vo.Author
+@JvmDefaultWithCompatibility
@Dao
interface DerivedDao : BaseDao<Author> {
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 64092d5..6b3ebcc 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -22,12 +22,6 @@
id("kotlin")
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
-}
-
dependencies {
api("androidx.annotation:annotation:1.3.0")
api(libs.kotlinStdlibJdk8)
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 251ca6d5..1056652 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -48,7 +48,6 @@
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += [
- "-Xjvm-default=all",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi",
"-opt-in=com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview"
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
index 227ba42..533b018 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
@@ -22,6 +22,8 @@
import androidx.room.compiler.codegen.kotlin.KotlinTypeSpec
import com.squareup.javapoet.JavaFile
import com.squareup.kotlinpoet.FileSpec
+import java.io.OutputStream
+import java.nio.file.Path
/**
* Code generation interface for XProcessing.
@@ -33,6 +35,20 @@
fun write(fileSpec: FileSpec, mode: Mode = Mode.Isolating)
/**
+ * Writes a resource file that will be part of the output artifact (e.g. jar).
+ *
+ * Only non-source files should be written via this function, if the file path corresponds to a
+ * source file, `.java` or `.kt` this function will throw an exception.
+ *
+ * @return the output stream to write the resource file.
+ */
+ fun writeResource(
+ filePath: Path,
+ originatingElements: List<XElement>,
+ mode: Mode = Mode.Isolating
+ ): OutputStream
+
+ /**
* Specifies whether a file represents aggregating or isolating inputs for incremental
* build purposes. This does not apply in Javac processing because aggregating vs isolating
* is set on the processor level. For more on KSP's definitions of isolating vs aggregating
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
index d61dc26c..93578c7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
@@ -16,11 +16,16 @@
package androidx.room.compiler.processing.javac
+import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XProcessingEnv
import com.squareup.javapoet.JavaFile
import com.squareup.kotlinpoet.FileSpec
+import java.io.OutputStream
+import java.nio.file.Path
import javax.annotation.processing.Filer
+import javax.tools.StandardLocation
+import kotlin.io.path.extension
internal class JavacFiler(
private val processingEnv: XProcessingEnv,
@@ -40,4 +45,24 @@
}
fileSpec.writeTo(delegate)
}
+
+ override fun writeResource(
+ filePath: Path,
+ originatingElements: List<XElement>,
+ mode: XFiler.Mode
+ ): OutputStream {
+ require(filePath.extension != "java" && filePath.extension != "kt") {
+ "Could not create resource file with a source type extension. File must not be " +
+ "neither '.java' nor '.kt', but was: $filePath"
+ }
+ val javaOriginatingElements =
+ originatingElements.filterIsInstance<JavacElement>().map { it.element }.toTypedArray()
+ val fileObject = delegate.createResource(
+ StandardLocation.CLASS_OUTPUT,
+ "",
+ filePath.toString(),
+ *javaOriginatingElements
+ )
+ return fileObject.openOutputStream()
+ }
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
index 68af580..1080617 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
@@ -17,24 +17,23 @@
package androidx.room.compiler.processing.ksp
import androidx.room.compiler.processing.InternalXAnnotated
-import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XAnnotation
+import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.unwrapRepeatedAnnotationsFromContainer
-import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSTypeAlias
+import java.lang.annotation.ElementType
import kotlin.reflect.KClass
-@OptIn(KspExperimental::class)
internal sealed class KspAnnotated(
val env: KspProcessingEnv
) : InternalXAnnotated {
abstract fun annotations(): Sequence<KSAnnotation>
private fun <T : Annotation> findAnnotations(annotation: KClass<T>): Sequence<KSAnnotation> {
- return annotations().filter { isSameAnnotationClass(it, annotation) }
+ return annotations().filter { it.isSameAnnotationClass(annotation) }
}
override fun getAllAnnotations(): List<XAnnotation> {
@@ -82,31 +81,19 @@
containerAnnotation: KClass<out Annotation>?
): Boolean {
return annotations().any {
- isSameAnnotationClass(it, annotation) ||
- (containerAnnotation != null && isSameAnnotationClass(it, containerAnnotation))
+ it.isSameAnnotationClass(annotation) ||
+ (containerAnnotation != null && it.isSameAnnotationClass(containerAnnotation))
}
}
- private fun isSameAnnotationClass(
- ksAnnotation: KSAnnotation,
- annotationClass: KClass<out Annotation>
- ): Boolean {
- var declaration = ksAnnotation.annotationType.resolve().declaration
- while (declaration is KSTypeAlias) {
- declaration = declaration.type.resolve().declaration
- }
- val qualifiedName = declaration.qualifiedName?.asString() ?: return false
- return qualifiedName == annotationClass.qualifiedName
- }
-
private class KSAnnotatedDelegate(
env: KspProcessingEnv,
private val delegate: KSAnnotated,
private val useSiteFilter: UseSiteFilter
) : KspAnnotated(env) {
override fun annotations(): Sequence<KSAnnotation> {
- return delegate.annotations.asSequence().filter {
- useSiteFilter.accept(it)
+ return delegate.annotations.filter {
+ useSiteFilter.accept(env, it)
}
}
}
@@ -118,50 +105,96 @@
}
/**
- * TODO: The implementation of UseSiteFilter is not 100% correct until
- * https://github.com/google/ksp/issues/96 is fixed.
- * https://kotlinlang.org/docs/reference/annotations.html
+ * Annotation use site filter
*
- * More specifically, when a use site is not defined in an annotation, we need to find the
- * declaration of the annotation and decide on the use site based on that.
- * Unfortunately, due to KSP issue #96, we cannot yet read values from a `@Target` annotation
- * which prevents implementing it correctly.
- *
- * Current implementation just approximates it which should work for Room.
+ * https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets
*/
interface UseSiteFilter {
- fun accept(annotation: KSAnnotation): Boolean
+ fun accept(env: KspProcessingEnv, annotation: KSAnnotation): Boolean
private class Impl(
- val acceptedTarget: AnnotationUseSiteTarget,
+ val acceptedSiteTarget: AnnotationUseSiteTarget,
+ val acceptedTarget: AnnotationTarget,
private val acceptNoTarget: Boolean = true,
) : UseSiteFilter {
- override fun accept(annotation: KSAnnotation): Boolean {
- val target = annotation.useSiteTarget
- return if (target == null) {
- acceptNoTarget
+ override fun accept(env: KspProcessingEnv, annotation: KSAnnotation): Boolean {
+ val useSiteTarget = annotation.useSiteTarget
+ val annotationTargets = annotation.getDeclaredTargets(env)
+ return if (useSiteTarget != null) {
+ acceptedSiteTarget == useSiteTarget
+ } else if (annotationTargets.isNotEmpty()) {
+ annotationTargets.contains(acceptedTarget)
} else {
- acceptedTarget == target
+ acceptNoTarget
}
}
}
companion object {
val NO_USE_SITE = object : UseSiteFilter {
- override fun accept(annotation: KSAnnotation): Boolean {
+ override fun accept(env: KspProcessingEnv, annotation: KSAnnotation): Boolean {
return annotation.useSiteTarget == null
}
}
- val NO_USE_SITE_OR_FIELD: UseSiteFilter = Impl(AnnotationUseSiteTarget.FIELD)
- val NO_USE_SITE_OR_METHOD_PARAMETER: UseSiteFilter =
- Impl(AnnotationUseSiteTarget.PARAM)
- val NO_USE_SITE_OR_GETTER: UseSiteFilter = Impl(AnnotationUseSiteTarget.GET)
- val NO_USE_SITE_OR_SETTER: UseSiteFilter = Impl(AnnotationUseSiteTarget.SET)
- val NO_USE_SITE_OR_SET_PARAM: UseSiteFilter = Impl(AnnotationUseSiteTarget.SETPARAM)
+ val NO_USE_SITE_OR_FIELD: UseSiteFilter = Impl(
+ acceptedSiteTarget = AnnotationUseSiteTarget.FIELD,
+ acceptedTarget = AnnotationTarget.FIELD
+ )
+ val NO_USE_SITE_OR_METHOD_PARAMETER: UseSiteFilter = Impl(
+ acceptedSiteTarget = AnnotationUseSiteTarget.PARAM,
+ acceptedTarget = AnnotationTarget.VALUE_PARAMETER
+ )
+ val NO_USE_SITE_OR_GETTER: UseSiteFilter = Impl(
+ acceptedSiteTarget = AnnotationUseSiteTarget.GET,
+ acceptedTarget = AnnotationTarget.PROPERTY_GETTER
+ )
+ val NO_USE_SITE_OR_SETTER: UseSiteFilter = Impl(
+ acceptedSiteTarget = AnnotationUseSiteTarget.SET,
+ acceptedTarget = AnnotationTarget.PROPERTY_SETTER
+ )
+ val NO_USE_SITE_OR_SET_PARAM: UseSiteFilter = Impl(
+ acceptedSiteTarget = AnnotationUseSiteTarget.SETPARAM,
+ acceptedTarget = AnnotationTarget.PROPERTY_SETTER
+ )
val FILE: UseSiteFilter = Impl(
- acceptedTarget = AnnotationUseSiteTarget.FILE,
+ acceptedSiteTarget = AnnotationUseSiteTarget.FILE,
+ acceptedTarget = AnnotationTarget.FILE,
acceptNoTarget = false
)
+
+ private fun KSAnnotation.getDeclaredTargets(
+ env: KspProcessingEnv
+ ): Set<AnnotationTarget> {
+ val annotationDeclaration = this.annotationType.resolve().declaration
+ val kotlinTargets = annotationDeclaration.annotations.firstOrNull {
+ it.isSameAnnotationClass(kotlin.annotation.Target::class)
+ }?.let { targetAnnotation ->
+ KspAnnotation(env, targetAnnotation)
+ .asAnnotationBox(kotlin.annotation.Target::class.java)
+ .value.allowedTargets
+ }?.toSet() ?: emptySet()
+ val javaTargets = annotationDeclaration.annotations.firstOrNull {
+ it.isSameAnnotationClass(java.lang.annotation.Target::class)
+ }?.let { targetAnnotation ->
+ KspAnnotation(env, targetAnnotation)
+ .asAnnotationBox(java.lang.annotation.Target::class.java)
+ .value.value.toList()
+ }?.mapNotNull { it.toAnnotationTarget() }?.toSet() ?: emptySet()
+ return kotlinTargets + javaTargets
+ }
+
+ private fun ElementType.toAnnotationTarget() = when (this) {
+ ElementType.TYPE -> AnnotationTarget.CLASS
+ ElementType.FIELD -> AnnotationTarget.FIELD
+ ElementType.METHOD -> AnnotationTarget.FUNCTION
+ ElementType.PARAMETER -> AnnotationTarget.VALUE_PARAMETER
+ ElementType.CONSTRUCTOR -> AnnotationTarget.CONSTRUCTOR
+ ElementType.LOCAL_VARIABLE -> AnnotationTarget.LOCAL_VARIABLE
+ ElementType.ANNOTATION_TYPE -> AnnotationTarget.ANNOTATION_CLASS
+ ElementType.TYPE_PARAMETER -> AnnotationTarget.TYPE_PARAMETER
+ ElementType.TYPE_USE -> AnnotationTarget.TYPE
+ else -> null
+ }
}
}
@@ -175,5 +208,16 @@
KSAnnotatedDelegate(env, it, filter)
} ?: NotAnnotated(env)
}
+
+ internal fun KSAnnotation.isSameAnnotationClass(
+ annotationClass: KClass<out Annotation>
+ ): Boolean {
+ var declaration = annotationType.resolve().declaration
+ while (declaration is KSTypeAlias) {
+ declaration = declaration.type.resolve().declaration
+ }
+ val qualifiedName = declaration.qualifiedName?.asString() ?: return false
+ return qualifiedName == annotationClass.qualifiedName
+ }
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
index 373d9607..7c92ac3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
@@ -16,8 +16,10 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XMessager
+import androidx.room.compiler.processing.originatingElementForPoet
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
@@ -27,8 +29,11 @@
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.OriginatingElementsHolder
import java.io.OutputStream
+import java.nio.file.Path
import javax.lang.model.element.Element
import javax.tools.Diagnostic
+import kotlin.io.path.extension
+import kotlin.io.path.nameWithoutExtension
internal class KspFiler(
private val delegate: CodeGenerator,
@@ -70,6 +75,27 @@
}
}
+ override fun writeResource(
+ filePath: Path,
+ originatingElements: List<XElement>,
+ mode: XFiler.Mode
+ ): OutputStream {
+ require(filePath.extension != "java" && filePath.extension != "kt") {
+ "Could not create resource file with a source type extension. File must not be " +
+ "neither '.java' nor '.kt', but was: $filePath"
+ }
+ val kspFilerOriginatingElements = originatingElements
+ .mapNotNull { it.originatingElementForPoet() }
+ .toOriginatingElements()
+ return createNewFile(
+ originatingElements = kspFilerOriginatingElements,
+ packageName = filePath.parent?.toString() ?: "",
+ fileName = filePath.nameWithoutExtension,
+ extensionName = filePath.extension,
+ aggregating = mode == XFiler.Mode.Aggregating
+ )
+ }
+
private fun createNewFile(
originatingElements: OriginatingElements,
packageName: String,
@@ -78,14 +104,16 @@
aggregating: Boolean
): OutputStream {
val dependencies = if (originatingElements.isEmpty()) {
- messager.printMessage(
- Diagnostic.Kind.WARNING,
- """
- No dependencies are reported for $fileName which will prevent
- incremental compilation.
- Please file a bug at $ISSUE_TRACKER_LINK.
- """.trimIndent()
- )
+ val isSourceFile = extensionName == "java" || extensionName == "kt"
+ if (isSourceFile) {
+ val filePath = "$packageName.$fileName.$extensionName"
+ messager.printMessage(
+ Diagnostic.Kind.WARNING,
+ "No dependencies reported for generated source $filePath which will" +
+ "prevent incremental compilation.\n" +
+ "Please file a bug at $ISSUE_TRACKER_LINK."
+ )
+ }
Dependencies.ALL_FILES
} else {
Dependencies(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
index b9c13a4..0fc88da 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
@@ -60,6 +60,12 @@
}
else -> error("Unsupported $symbol with annotation $annotationQualifiedName")
}
+ }.filter {
+ // Due to the bug in https://github.com/google/ksp/issues/1198, KSP may incorrectly
+ // copy annotations from a constructor KSValueParameter to its KSPropertyDeclaration
+ // which we remove manually, so check here to make sure this is in sync with the
+ // actual annotations on the element.
+ it.getAllAnnotations().any { it.qualifiedName == annotationQualifiedName }
}.toSet()
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
index 889a878..d96e81d 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
@@ -34,6 +34,7 @@
import androidx.room.compiler.processing.util.asJTypeName
import androidx.room.compiler.processing.util.asKTypeName
import androidx.room.compiler.processing.util.compileFiles
+import androidx.room.compiler.processing.util.getDeclaredField
import androidx.room.compiler.processing.util.getField
import androidx.room.compiler.processing.util.getMethodByJvmName
import androidx.room.compiler.processing.util.getParameter
@@ -49,6 +50,7 @@
// used in typealias test
typealias OtherAnnotationTypeAlias = OtherAnnotation
+
@RunWith(Parameterized::class)
class XAnnotationTest(
private val preCompiled: Boolean
@@ -963,6 +965,147 @@
}
}
+ // This is testing the workaround for https://github.com/google/ksp/issues/1198
+ @Test
+ fun paramTargetInPrimaryCtorProperty() {
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
+ class Subject(
+ @MyAnnotation field: String,
+ @MyAnnotation val valField: String,
+ @MyAnnotation var varField: String,
+ )
+ @Target(AnnotationTarget.VALUE_PARAMETER)
+ annotation class MyAnnotation
+ """.trimIndent()
+ )),
+ ) { invocation ->
+ // Verifies the KspRoundEnv side of the workaround.
+ if (!preCompiled) {
+ val annotatedElements =
+ invocation.roundEnv.getElementsAnnotatedWith("test.MyAnnotation")
+ assertThat(annotatedElements.all { it is XExecutableParameterElement }).isTrue()
+ assertThat(annotatedElements.map { it.name })
+ .containsExactly("field", "valField", "varField")
+ }
+
+ val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+ val myAnnotation = invocation.processingEnv.requireTypeElement("test.MyAnnotation")
+
+ val constructorParameters = subject.getConstructors().single().parameters
+ assertThat(constructorParameters.map { it.name })
+ .containsExactly("field", "valField", "varField")
+ fun getCtorParameterAnnotationElements(paramName: String): List<XTypeElement> {
+ return constructorParameters
+ .first { it.name == paramName }
+ .getAllAnnotations()
+ .map(XAnnotation::typeElement)
+ }
+ assertThat(getCtorParameterAnnotationElements("field")).contains(myAnnotation)
+ assertThat(getCtorParameterAnnotationElements("valField")).contains(myAnnotation)
+ assertThat(getCtorParameterAnnotationElements("varField")).contains(myAnnotation)
+
+ assertThat(subject.getDeclaredFields().map(XFieldElement::name))
+ .containsExactly("valField", "varField")
+ fun getDeclaredFieldAnnotationElements(fieldName: String): List<XTypeElement> {
+ return subject.getDeclaredField(fieldName)
+ .getAllAnnotations()
+ .map(XAnnotation::typeElement)
+ }
+ assertThat(getDeclaredFieldAnnotationElements("valField")).doesNotContain(myAnnotation)
+ assertThat(getDeclaredFieldAnnotationElements("varField")).doesNotContain(myAnnotation)
+ }
+ }
+
+ @Test
+ fun fieldTargetInPrimaryCtorProperty() {
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
+ class Subject(
+ @MyAnnotation val valField: String,
+ @MyAnnotation var varField: String,
+ )
+ @Target(AnnotationTarget.FIELD)
+ annotation class MyAnnotation
+ """.trimIndent()
+ )),
+ ) { invocation ->
+ val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+ val myAnnotation = invocation.processingEnv.requireTypeElement("test.MyAnnotation")
+
+ val constructorParameters = subject.getConstructors().single().parameters
+ assertThat(constructorParameters.map { it.name })
+ .containsExactly("valField", "varField")
+ fun getCtorParameterAnnotationElements(paramName: String): List<XTypeElement> {
+ return constructorParameters
+ .first { it.name == paramName }
+ .getAllAnnotations()
+ .map(XAnnotation::typeElement)
+ }
+ assertThat(getCtorParameterAnnotationElements("valField")).doesNotContain(myAnnotation)
+ assertThat(getCtorParameterAnnotationElements("varField")).doesNotContain(myAnnotation)
+
+ assertThat(subject.getDeclaredFields().map(XFieldElement::name))
+ .containsExactly("valField", "varField")
+ fun getDeclaredFieldAnnotationElements(fieldName: String): List<XTypeElement> {
+ return subject.getDeclaredField(fieldName)
+ .getAllAnnotations()
+ .map(XAnnotation::typeElement)
+ }
+ assertThat(getDeclaredFieldAnnotationElements("valField")).contains(myAnnotation)
+ assertThat(getDeclaredFieldAnnotationElements("varField")).contains(myAnnotation)
+ }
+ }
+
+ @Test
+ fun propertyTargetInPrimaryCtorProperty() {
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
+ class Subject(
+ @MyAnnotation val valField: String,
+ @MyAnnotation var varField: String,
+ )
+ @Target(AnnotationTarget.PROPERTY)
+ annotation class MyAnnotation
+ """.trimIndent()
+ )),
+ ) { invocation ->
+ val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+ val myAnnotation = invocation.processingEnv.requireTypeElement("test.MyAnnotation")
+
+ val constructorParameters = subject.getConstructors().single().parameters
+ assertThat(constructorParameters.map { it.name })
+ .containsExactly("valField", "varField")
+ fun getCtorParameterAnnotationElements(paramName: String): List<XTypeElement> {
+ return constructorParameters
+ .first { it.name == paramName }
+ .getAllAnnotations()
+ .map(XAnnotation::typeElement)
+ }
+ assertThat(getCtorParameterAnnotationElements("valField")).doesNotContain(myAnnotation)
+ assertThat(getCtorParameterAnnotationElements("varField")).doesNotContain(myAnnotation)
+
+ assertThat(subject.getDeclaredFields().map(XFieldElement::name))
+ .containsExactly("valField", "varField")
+ fun getDeclaredFieldAnnotationElements(fieldName: String): List<XTypeElement> {
+ return subject.getDeclaredField(fieldName)
+ .getAllAnnotations()
+ .map(XAnnotation::typeElement)
+ }
+ assertThat(getDeclaredFieldAnnotationElements("valField")).doesNotContain(myAnnotation)
+ assertThat(getDeclaredFieldAnnotationElements("varField")).doesNotContain(myAnnotation)
+ }
+ }
+
// helper function to read what we need
private fun XAnnotated.getSuppressValues(): List<String>? {
return this.findAnnotation<TestSuppressWarnings>()
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt
new file mode 100644
index 0000000..f69f171
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.io.path.Path
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class XFilerTest {
+
+ @Test
+ fun writeResource_sourceFile_java() {
+ runProcessorTest {
+ try {
+ it.processingEnv.filer.writeResource(Path("Test.java"), emptyList()).close()
+ fail("Expected an exception!")
+ } catch (ex: java.lang.IllegalArgumentException) {
+ assertThat(ex.message).isEqualTo(
+ "Could not create resource file with a source type extension. " +
+ "File must not be neither '.java' nor '.kt', but was: Test.java"
+ )
+ }
+ }
+ }
+
+ @Test
+ fun writeResource_sourceFile_kotlin() {
+ runProcessorTest {
+ try {
+ it.processingEnv.filer.writeResource(Path("Test.kt"), emptyList()).close()
+ fail("Expected an exception!")
+ } catch (ex: IllegalArgumentException) {
+ assertThat(ex.message).isEqualTo(
+ "Could not create resource file with a source type extension. " +
+ "File must not be neither '.java' nor '.kt', but was: Test.kt"
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 3802cb2..c9ec393 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -41,10 +41,46 @@
import com.squareup.kotlinpoet.javapoet.KTypeVariableName
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import org.junit.runners.Parameterized
-@RunWith(JUnit4::class)
-class XTypeElementTest {
+@RunWith(Parameterized::class)
+class XTypeElementTest(
+ private val isPreCompiled: Boolean,
+) {
+ private fun runTest(
+ sources: List<Source>,
+ handler: (XTestInvocation) -> Unit
+ ) {
+ if (isPreCompiled) {
+ val compiled = compileFiles(sources)
+ val hasKotlinSources = sources.any {
+ it is Source.KotlinSource
+ }
+ val kotlinSources = if (hasKotlinSources) {
+ listOf(
+ Source.kotlin("placeholder.kt", "class PlaceholderKotlin")
+ )
+ } else {
+ emptyList()
+ }
+ val newSources = kotlinSources + Source.java(
+ "PlaceholderJava",
+ "public class " +
+ "PlaceholderJava {}"
+ )
+ runProcessorTest(
+ sources = newSources,
+ handler = handler,
+ classpath = compiled
+ )
+ } else {
+ runProcessorTest(
+ sources = sources,
+ handler = handler
+ )
+ }
+ }
+
@Test
fun qualifiedNames() {
val src1 = Source.kotlin(
@@ -70,7 +106,7 @@
}
"""
)
- runProcessorTest(
+ runTest(
sources = listOf(src1, src2, src3)
) { invocation ->
invocation.processingEnv.requireTypeElement("TopLevel").let {
@@ -140,54 +176,54 @@
interface AnotherInterface : MyInterface
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
- assertThat(it.superClass).isEqualTo(
- invocation.processingEnv.requireType("foo.bar.AbstractClass")
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(
+ invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName()
)
- assertThat(it.type.superTypes).containsExactly(
- invocation.processingEnv.requireType("foo.bar.AbstractClass"),
- invocation.processingEnv.requireType("foo.bar.MyInterface")
+ assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+ invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName(),
+ invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
)
- assertThat(it.type).isEqualTo(
- invocation.processingEnv.requireType("foo.bar.Baz")
+ assertThat(it.type.asTypeName()).isEqualTo(
+ invocation.processingEnv.requireType("foo.bar.Baz").asTypeName()
)
assertThat(it.isInterface()).isFalse()
assertThat(it.isKotlinObject()).isFalse()
assertThat(it.isAbstract()).isFalse()
}
invocation.processingEnv.requireTypeElement("foo.bar.AbstractClass").let {
- assertThat(it.superClass).isEqualTo(
- invocation.processingEnv.requireType(JTypeName.OBJECT)
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(
+ invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
)
- assertThat(it.type.superTypes).containsExactly(
- invocation.processingEnv.requireType(JTypeName.OBJECT)
+ assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+ invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
)
assertThat(it.isAbstract()).isTrue()
assertThat(it.isInterface()).isFalse()
- assertThat(it.type).isEqualTo(
- invocation.processingEnv.requireType("foo.bar.AbstractClass")
+ assertThat(it.type.asTypeName()).isEqualTo(
+ invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName()
)
}
invocation.processingEnv.requireTypeElement("foo.bar.MyInterface").let {
assertThat(it.superClass).isNull()
- assertThat(it.type.superTypes).containsExactly(
- invocation.processingEnv.requireType(JTypeName.OBJECT)
+ assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+ invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
)
assertThat(it.isInterface()).isTrue()
- assertThat(it.type).isEqualTo(
- invocation.processingEnv.requireType("foo.bar.MyInterface")
+ assertThat(it.type.asTypeName()).isEqualTo(
+ invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
)
}
invocation.processingEnv.requireTypeElement("foo.bar.AnotherInterface").let {
assertThat(it.superClass).isNull()
- assertThat(it.type.superTypes).containsExactly(
- invocation.processingEnv.requireType("java.lang.Object"),
- invocation.processingEnv.requireType("foo.bar.MyInterface")
+ assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
+ invocation.processingEnv.requireType("java.lang.Object").asTypeName(),
+ invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
)
assertThat(it.isInterface()).isTrue()
- assertThat(it.type).isEqualTo(
- invocation.processingEnv.requireType("foo.bar.AnotherInterface")
+ assertThat(it.type.asTypeName()).isEqualTo(
+ invocation.processingEnv.requireType("foo.bar.AnotherInterface").asTypeName()
)
}
}
@@ -204,7 +240,7 @@
interface MyInterface<E>
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
assertThat(it.superInterfaces).hasSize(1)
val superInterface = it.superInterfaces.first { type ->
@@ -232,7 +268,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Outer").let {
assertThat(it.asClassName())
.isEqualTo(XClassName.get("foo.bar", "Outer"))
@@ -291,7 +327,7 @@
}
""".trimIndent()
)
- runProcessorTest(
+ runTest(
sources = listOf(kotlinSrc, javaSrc, javaAnnotationSrc)
) { invocation ->
fun getModifiers(element: XTypeElement): Set<String> {
@@ -346,8 +382,15 @@
.containsExactly("public", "class")
assertThat(getModifiers("OuterJavaClass.NestedJavaClass"))
.containsExactly("public", "static", "class")
- assertThat(getModifiers("JavaAnnotation"))
- .containsExactly("abstract", "public", "annotation")
+ assertThat(getModifiers("JavaAnnotation")).apply {
+ // KSP vs KAPT metadata have a difference in final vs abstract modifiers
+ // for annotation types.
+ if (isPreCompiled && invocation.isKsp) {
+ containsExactly("final", "public", "annotation")
+ } else {
+ containsExactly("abstract", "public", "annotation")
+ }
+ }
assertThat(getModifiers("KotlinAnnotation")).apply {
// KSP vs KAPT metadata have a difference in final vs abstract modifiers
// for annotation types.
@@ -359,8 +402,13 @@
}
assertThat(getModifiers("DataClass"))
.containsExactly("public", "final", "class", "data")
- assertThat(getModifiers("InlineClass"))
- .containsExactly("public", "final", "class", "value")
+ assertThat(getModifiers("InlineClass")).apply {
+ if (isPreCompiled && invocation.isKsp) {
+ containsExactly("public", "final", "class")
+ } else {
+ containsExactly("public", "final", "class", "value")
+ }
+ }
assertThat(getModifiers("FunInterface"))
.containsExactly("public", "abstract", "interface", "fun")
}
@@ -375,7 +423,7 @@
interface MyInterface
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("MyClass").let {
assertThat(it.kindName()).isEqualTo("class")
}
@@ -398,7 +446,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
assertThat(baseClass.getAllFieldNames()).containsExactly("genericProp")
assertThat(baseClass.getDeclaredFields().map { it.name })
@@ -439,7 +487,7 @@
) : BaseClass(value)
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
assertThat(baseClass.getAllFieldNames()).containsExactly("value")
val subClass = invocation.processingEnv.requireTypeElement("SubClass")
@@ -465,10 +513,11 @@
@Test
fun fieldsMethodsWithoutBacking() {
- fun buildSrc(pkg: String) = Source.kotlin(
- "Foo.kt",
- """
- package $pkg
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
class Subject {
val realField: String = ""
get() = field
@@ -492,42 +541,37 @@
}
}
""".trimIndent()
- )
- val lib = compileFiles(listOf(buildSrc("lib")))
- runProcessorTest(
- sources = listOf(buildSrc("main")),
- classpath = lib
+ )),
) { invocation ->
- listOf("lib", "main").forEach { pkg ->
- val subject = invocation.processingEnv.requireTypeElement("$pkg.Subject")
- val declaredFields = subject.getDeclaredFields().map { it.name } -
- listOf("Companion") // skip Companion, KAPT generates it
- val expectedFields = listOf("realField", "staticRealField")
- assertWithMessage(subject.qualifiedName)
- .that(declaredFields)
- .containsExactlyElementsIn(expectedFields)
- val allFields = subject.getAllFieldsIncludingPrivateSupers().map { it.name } -
- listOf("Companion") // skip Companion, KAPT generates it
- assertWithMessage(subject.qualifiedName)
- .that(allFields.toList())
- .containsExactlyElementsIn(expectedFields)
- val methodNames = subject.getDeclaredMethods().map { it.jvmName }
- assertWithMessage(subject.qualifiedName)
- .that(methodNames)
- .containsAtLeast("getNoBackingVal", "getNoBackingVar", "setNoBackingVar")
- assertWithMessage(subject.qualifiedName)
- .that(methodNames)
- .doesNotContain("setNoBackingVal")
- }
+ val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+ val declaredFields = subject.getDeclaredFields().map { it.name } -
+ listOf("Companion") // skip Companion, KAPT generates it
+ val expectedFields = listOf("realField", "staticRealField")
+ assertWithMessage(subject.qualifiedName)
+ .that(declaredFields)
+ .containsExactlyElementsIn(expectedFields)
+ val allFields = subject.getAllFieldsIncludingPrivateSupers().map { it.name } -
+ listOf("Companion") // skip Companion, KAPT generates it
+ assertWithMessage(subject.qualifiedName)
+ .that(allFields.toList())
+ .containsExactlyElementsIn(expectedFields)
+ val methodNames = subject.getDeclaredMethods().map { it.jvmName }
+ assertWithMessage(subject.qualifiedName)
+ .that(methodNames)
+ .containsAtLeast("getNoBackingVal", "getNoBackingVar", "setNoBackingVar")
+ assertWithMessage(subject.qualifiedName)
+ .that(methodNames)
+ .doesNotContain("setNoBackingVal")
}
}
@Test
fun abstractFields() {
- fun buildSource(pkg: String) = Source.kotlin(
- "Foo.kt",
- """
- package $pkg
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
abstract class Subject {
val value: String = ""
abstract val abstractValue: String
@@ -538,57 +582,46 @@
}
}
""".trimIndent()
- )
-
- val lib = compileFiles(listOf(buildSource("lib")))
- runProcessorTest(
- sources = listOf(buildSource("main")),
- classpath = lib
+ )),
) { invocation ->
- listOf("lib", "main").forEach { pkg ->
- val subject = invocation.processingEnv.requireTypeElement("$pkg.Subject")
- val declaredFields = subject.getDeclaredFields().map { it.name } -
- listOf("Companion")
- val expectedFields = listOf("value", "realCompanion", "jvmStatic")
- assertWithMessage(subject.qualifiedName)
- .that(declaredFields)
- .containsExactlyElementsIn(expectedFields)
- }
+ val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+ val declaredFields = subject.getDeclaredFields().map { it.name } -
+ listOf("Companion")
+ val expectedFields = listOf("value", "realCompanion", "jvmStatic")
+ assertWithMessage(subject.qualifiedName)
+ .that(declaredFields)
+ .containsExactlyElementsIn(expectedFields)
}
}
@Test
fun lateinitFields() {
- fun buildSource(pkg: String) = Source.kotlin(
- "Foo.kt",
- """
- package $pkg
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
class Subject {
lateinit var x:String
var y:String = "abc"
}
""".trimIndent()
- )
- runProcessorTest(
- sources = listOf(buildSource("app")),
- classpath = compileFiles(listOf(buildSource("lib")))
+ )),
) { invocation ->
- listOf("app", "lib").forEach { pkg ->
- val subject = invocation.processingEnv.requireTypeElement("$pkg.Subject")
- assertWithMessage(subject.fallbackLocationText)
- .that(subject.getDeclaredFields().map { it.name })
- .containsExactly(
- "x", "y"
- )
- assertWithMessage(subject.fallbackLocationText)
- .that(subject.getDeclaredMethods().map { it.jvmName })
- .containsExactly(
- "getX", "setX", "getY", "setY"
- )
- subject.getField("x").let { field ->
- assertThat(field.isFinal()).isFalse()
- assertThat(field.isPrivate()).isFalse()
- }
+ val subject = invocation.processingEnv.requireTypeElement("test.Subject")
+ assertWithMessage(subject.fallbackLocationText)
+ .that(subject.getDeclaredFields().map { it.name })
+ .containsExactly(
+ "x", "y"
+ )
+ assertWithMessage(subject.fallbackLocationText)
+ .that(subject.getDeclaredMethods().map { it.jvmName })
+ .containsExactly(
+ "getX", "setX", "getY", "setY"
+ )
+ subject.getField("x").let { field ->
+ assertThat(field.isFinal()).isFalse()
+ assertThat(field.isPrivate()).isFalse()
}
}
}
@@ -603,7 +636,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val element = invocation.processingEnv.requireTypeElement("MyInterface")
assertThat(element.getAllFieldsIncludingPrivateSupers().toList()).isEmpty()
element.getMethodByJvmName("getX").let {
@@ -628,7 +661,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val element = invocation.processingEnv.requireTypeElement("MyAbstractClass")
assertThat(
element.getAllFieldNames()
@@ -659,61 +692,34 @@
@Test
fun propertyGettersSetters() {
- val dependencyJavaSource = Source.java(
- "DependencyJavaSubject.java",
- """
- class DependencyJavaSubject {
- int myField;
- private int mutable;
- int immutable;
- int getMutable() {return 3;}
- void setMutable(int x) {}
- int getImmutable() {return 3;}
- }
- """.trimIndent()
- )
- val dependencyKotlinSource = Source.kotlin(
- "DependencyKotlinSubject.kt",
- """
- class DependencyKotlinSubject {
- private val myField = 0
- var mutable: Int = 0
- val immutable:Int = 0
- }
- """.trimIndent()
- )
- val dependency = compileFiles(listOf(dependencyJavaSource, dependencyKotlinSource))
- val javaSource = Source.java(
- "JavaSubject.java",
- """
- class JavaSubject {
- int myField;
- private int mutable;
- int immutable;
- int getMutable() {return 3;}
- void setMutable(int x) {}
- int getImmutable() {return 3;}
- }
- """.trimIndent()
- )
- val kotlinSource = Source.kotlin(
- "KotlinSubject.kt",
- """
- class KotlinSubject {
- private val myField = 0
- var mutable: Int = 0
- val immutable:Int = 0
- }
- """.trimIndent()
- )
- runProcessorTest(
- listOf(javaSource, kotlinSource),
- classpath = dependency
- ) { invocation ->
+ runTest(
listOf(
- "JavaSubject", "DependencyJavaSubject",
- "KotlinSubject", "DependencyKotlinSubject"
- ).map {
+ Source.java(
+ "JavaSubject.java",
+ """
+ class JavaSubject {
+ int myField;
+ private int mutable;
+ int immutable;
+ int getMutable() {return 3;}
+ void setMutable(int x) {}
+ int getImmutable() {return 3;}
+ }
+ """.trimIndent()
+ ),
+ Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ class KotlinSubject {
+ private val myField = 0
+ var mutable: Int = 0
+ val immutable:Int = 0
+ }
+ """.trimIndent()
+ )
+ ),
+ ) { invocation ->
+ listOf("JavaSubject", "KotlinSubject",).map {
invocation.processingEnv.requireTypeElement(it)
}.forEach { subject ->
val methods = subject.getDeclaredMethods()
@@ -770,7 +776,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("Base")
val objectMethodNames = invocation.objectMethodNames()
assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
@@ -791,10 +797,11 @@
@Test
fun diamondOverride() {
- fun buildSrc(pkg: String) = Source.kotlin(
- "Foo.kt",
- """
- package $pkg
+ runTest(
+ sources = listOf(Source.kotlin(
+ "Foo.kt",
+ """
+ package test
interface Parent<T> {
fun parent(t: T)
}
@@ -813,38 +820,32 @@
abstract override fun parent(t: String)
}
""".trimIndent()
- )
-
- runProcessorTest(
- sources = listOf(buildSrc("app")),
- classpath = compileFiles(listOf(buildSrc("lib")))
+ )),
) { invocation ->
- listOf("lib", "app").forEach { pkg ->
- invocation.processingEnv.requireTypeElement("$pkg.Subject1").let { subject ->
- assertWithMessage(subject.qualifiedName).that(
- invocation.nonObjectMethodSignatures(subject)
- ).containsExactly(
- "child1(java.lang.String):void",
- "child2(java.lang.String):void",
- "parent(java.lang.String):void",
- )
- }
- invocation.processingEnv.requireTypeElement("$pkg.Subject2").let { subject ->
- assertWithMessage(subject.qualifiedName).that(
- invocation.nonObjectMethodSignatures(subject)
- ).containsExactly(
- "child1(java.lang.String):void",
- "parent(java.lang.String):void",
- )
- }
- invocation.processingEnv.requireTypeElement("$pkg.Subject3").let { subject ->
- assertWithMessage(subject.qualifiedName).that(
- invocation.nonObjectMethodSignatures(subject)
- ).containsExactly(
- "child1(java.lang.String):void",
- "parent(java.lang.String):void",
- )
- }
+ invocation.processingEnv.requireTypeElement("test.Subject1").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ invocation.nonObjectMethodSignatures(subject)
+ ).containsExactly(
+ "child1(java.lang.String):void",
+ "child2(java.lang.String):void",
+ "parent(java.lang.String):void",
+ )
+ }
+ invocation.processingEnv.requireTypeElement("test.Subject2").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ invocation.nonObjectMethodSignatures(subject)
+ ).containsExactly(
+ "child1(java.lang.String):void",
+ "parent(java.lang.String):void",
+ )
+ }
+ invocation.processingEnv.requireTypeElement("test.Subject3").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ invocation.nonObjectMethodSignatures(subject)
+ ).containsExactly(
+ "child1(java.lang.String):void",
+ "parent(java.lang.String):void",
+ )
}
}
}
@@ -871,7 +872,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("DerivedInterface")
val methodNames = base.getAllMethods().toList().jvmNames()
assertThat(methodNames).containsExactly(
@@ -899,7 +900,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("DerivedClass")
val methodNamesCount =
base.getAllMethods().toList().jvmNames().groupingBy { it }.eachCount()
@@ -930,7 +931,7 @@
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement(
"ParentWithExplicitOverride"
).let { parent ->
@@ -990,7 +991,7 @@
""".trimIndent()
),
)
- runProcessorTest(sources = srcs) { invocation ->
+ runTest(sources = srcs) { invocation ->
invocation.processingEnv.requireTypeElement("foo.child.FooChild")
.let { fooChild ->
assertWithMessage(fooChild.qualifiedName).that(
@@ -1046,7 +1047,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val objectMethodNames = invocation.objectMethodNames()
val klass = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(
@@ -1073,11 +1074,12 @@
*/
@Test
fun allMethods_withJvmNames() {
- fun buildSource(pkg: String) = listOf(
- Source.kotlin(
- "Foo.kt",
- """
- package $pkg
+ runTest(
+ sources = listOf(
+ Source.kotlin(
+ "Foo.kt",
+ """
+ package test
interface Interface {
fun f1()
@JvmName("notF2")
@@ -1091,33 +1093,27 @@
}
}
""".trimIndent()
- )
- )
-
- runProcessorTest(
- sources = buildSource("app"),
- classpath = compileFiles(buildSource("lib"))
+ )
+ ),
) { invocation ->
- listOf("app", "lib").forEach {
- val appSubject = invocation.processingEnv.requireTypeElement("$it.Subject")
- val methodNames = appSubject.getAllMethods().map { it.name }.toList()
- val methodJvmNames = appSubject.getAllMethods().map { it.jvmName }.toList()
- val objectMethodNames = invocation.objectMethodNames()
- if (invocation.isKsp) {
- assertThat(methodNames - objectMethodNames).containsExactly(
- "f1", "f2"
- )
- assertThat(methodJvmNames - objectMethodNames).containsExactly(
- "notF1", "notF2"
- )
- } else {
- assertThat(methodNames - objectMethodNames).containsExactly(
- "f1", "f1", "f2"
- )
- assertThat(methodJvmNames - objectMethodNames).containsExactly(
- "f1", "notF1", "notF2"
- )
- }
+ val appSubject = invocation.processingEnv.requireTypeElement("test.Subject")
+ val methodNames = appSubject.getAllMethods().map { it.name }.toList()
+ val methodJvmNames = appSubject.getAllMethods().map { it.jvmName }.toList()
+ val objectMethodNames = invocation.objectMethodNames()
+ if (invocation.isKsp) {
+ assertThat(methodNames - objectMethodNames).containsExactly(
+ "f1", "f2"
+ )
+ assertThat(methodJvmNames - objectMethodNames).containsExactly(
+ "notF1", "notF2"
+ )
+ } else {
+ assertThat(methodNames - objectMethodNames).containsExactly(
+ "f1", "f1", "f2"
+ )
+ assertThat(methodJvmNames - objectMethodNames).containsExactly(
+ "f1", "notF1", "notF2"
+ )
}
}
}
@@ -1137,7 +1133,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val objectMethodNames = invocation.objectMethodNames()
invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
@@ -1195,7 +1191,7 @@
class SubClass : CompanionSubject()
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val objectMethodNames = invocation.processingEnv.requireTypeElement(
Any::class
).getAllMethods().jvmNames()
@@ -1270,7 +1266,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
"getX"
@@ -1319,7 +1315,7 @@
abstract class AbstractExplicit(x:Int)
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
@@ -1375,7 +1371,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("MyInterface")
assertThat(subject.getMethodByJvmName("notJvmDefault").isJavaDefault()).isFalse()
assertThat(subject.getMethodByJvmName("jvmDefault").isJavaDefault()).isTrue()
@@ -1422,7 +1418,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
@@ -1454,48 +1450,41 @@
@Test
fun enumTypeElement() {
- fun createSources(packageName: String) = listOf(
- Source.kotlin(
- "$packageName/KotlinEnum.kt",
- """
- package $packageName
- enum class KotlinEnum(private val x:Int) {
- VAL1(1),
- VAL2(2);
+ runTest(
+ sources = listOf(
+ Source.kotlin(
+ "test/KotlinEnum.kt",
+ """
+ package test
+ enum class KotlinEnum(private val x:Int) {
+ VAL1(1),
+ VAL2(2);
- fun enumMethod(): Unit {}
- }
- """.trimIndent()
- ),
- Source.java(
- "$packageName.JavaEnum",
- """
- package $packageName;
- public enum JavaEnum {
- VAL1(1),
- VAL2(2);
-
- private int x;
-
- JavaEnum(int x) {
- this.x = x;
+ fun enumMethod(): Unit {}
}
- void enumMethod() {}
- }
- """.trimIndent()
- )
- )
+ """.trimIndent()
+ ),
+ Source.java(
+ "test.JavaEnum",
+ """
+ package test;
+ public enum JavaEnum {
+ VAL1(1),
+ VAL2(2);
- val classpath = compileFiles(
- createSources("lib")
- )
- runProcessorTest(
- sources = createSources("app"),
- classpath = classpath
+ private int x;
+
+ JavaEnum(int x) {
+ this.x = x;
+ }
+ void enumMethod() {}
+ }
+ """.trimIndent()
+ )
+ ),
) { invocation ->
listOf(
- "lib.KotlinEnum", "lib.JavaEnum",
- "app.KotlinEnum", "app.JavaEnum"
+ "test.KotlinEnum", "test.JavaEnum",
).forEach { qName ->
val typeElement = invocation.processingEnv.requireTypeElement(qName)
assertWithMessage("$qName is enum")
@@ -1541,7 +1530,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val topLevelClass = invocation.processingEnv.requireTypeElement("TopLevelClass")
val enclosedTypeElements = topLevelClass.getEnclosedTypeElements()
@@ -1571,7 +1560,7 @@
}
""".trimIndent()
)
- runProcessorTest(sources = listOf(src)) { invocation ->
+ runTest(sources = listOf(src)) { invocation ->
val topLevelClass = invocation.processingEnv.requireTypeElement("TopLevelClass")
val enclosedTypeElements = topLevelClass.getEnclosedTypeElements()
@@ -1597,7 +1586,7 @@
}
""".trimIndent()
)
- runProcessorTest(listOf(kotlinSrc)) { invocation ->
+ runTest(listOf(kotlinSrc)) { invocation ->
val kotlinClass = invocation.processingEnv.requireTypeElement(
"foo.bar.KotlinClass")
val companionObjects = kotlinClass.getEnclosedTypeElements().filter {
@@ -1611,7 +1600,7 @@
@Test
fun inheritedGenericMethod() {
- runProcessorTest(
+ runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
@@ -1678,7 +1667,7 @@
@Test
fun overriddenGenericMethod() {
- runProcessorTest(
+ runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
@@ -1789,7 +1778,7 @@
@Test
fun overriddenGenericConstructor() {
- runProcessorTest(
+ runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
@@ -1832,7 +1821,7 @@
@Test
fun inheritedGenericField() {
- runProcessorTest(
+ runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
@@ -1904,4 +1893,12 @@
typeElement.getAllMethods()
.filterNot { it.jvmName in objectMethodNames() }
.map { it.signature(typeElement.type) }.toList()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "isPreCompiled_{0}")
+ fun params(): List<Array<Any>> {
+ return listOf(arrayOf(false), arrayOf(true))
+ }
+ }
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index 4075f36..62c1489 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -32,10 +32,11 @@
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
-import org.junit.Test
import java.io.File
import java.io.OutputStream
import javax.tools.Diagnostic
+import kotlin.io.path.Path
+import org.junit.Test
class KspFilerTest {
@@ -143,6 +144,29 @@
}
}
+ @Test
+ fun writeResource() {
+ runKspTest(
+ sources = emptyList()
+ ) { invocation ->
+ invocation.processingEnv.filer.writeResource(
+ filePath = Path("test.log"),
+ originatingElements = emptyList()
+ ).bufferedWriter(Charsets.UTF_8).use {
+ it.write("Hello!")
+ }
+ invocation.processingEnv.filer.writeResource(
+ filePath = Path("META-INF/services/com.test.Foo"),
+ originatingElements = emptyList()
+ ).bufferedWriter(Charsets.UTF_8).use {
+ it.write("Not a real service...")
+ }
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
+ }
+
private fun Dependencies?.containsExactlySimpleKotlinClass() {
assertThat(this).isNotNull()
val originatingFiles = this!!.originatingFiles.map { it.fileName }
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 571dda1..dbf84e5 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -281,7 +281,6 @@
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += [
- "-Xjvm-default=all",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi",
"-opt-in=com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview"
diff --git a/room/room-ktx/build.gradle b/room/room-ktx/build.gradle
index d824294..b4ed97d 100644
--- a/room/room-ktx/build.gradle
+++ b/room/room-ktx/build.gradle
@@ -51,10 +51,3 @@
android {
namespace "androidx.room.ktx"
}
-
-// Allow usage of Kotlin's @OptIn.
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
-}
\ No newline at end of file
diff --git a/room/room-migration/build.gradle b/room/room-migration/build.gradle
index 6fa47eaf..4924037 100644
--- a/room/room-migration/build.gradle
+++ b/room/room-migration/build.gradle
@@ -22,12 +22,6 @@
id("kotlin")
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
-}
-
dependencies {
implementation(project(":room:room-common"))
implementation(libs.kotlinStdlib)
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 49b6c29..1d8ce5d 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -30,9 +30,6 @@
consumerProguardFiles "proguard-rules.pro"
}
namespace "androidx.room"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
dependencies {
diff --git a/room/room-testing/build.gradle b/room/room-testing/build.gradle
index 39e53dd..7f0951e 100644
--- a/room/room-testing/build.gradle
+++ b/room/room-testing/build.gradle
@@ -24,10 +24,6 @@
android {
namespace "androidx.room.testing"
-
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
dependencies {
diff --git a/settings.gradle b/settings.gradle
index 31e3e3c..afcd43e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -376,8 +376,8 @@
includeProject(":appsearch:appsearch-local-storage", [BuildType.MAIN])
includeProject(":appsearch:appsearch-platform-storage", [BuildType.MAIN])
includeProject(":appsearch:appsearch-test-util", [BuildType.MAIN])
-includeProject(":arch:core:core-common", [BuildType.MAIN])
-includeProject(":arch:core:core-runtime", [BuildType.MAIN])
+includeProject(":arch:core:core-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":arch:core:core-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
includeProject(":arch:core:core-testing", [BuildType.MAIN])
includeProject(":asynclayoutinflater:asynclayoutinflater", [BuildType.MAIN])
includeProject(":asynclayoutinflater:asynclayoutinflater-appcompat", [BuildType.MAIN])
@@ -901,8 +901,12 @@
includeProject(":wear:compose:compose-foundation", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-foundation-samples", "wear/compose/compose-foundation/samples", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-material", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-material-benchmark", "wear/compose/compose-material/benchmark", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3-benchmark", "wear/compose/compose-material3/benchmark", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material-core", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-material-samples", "wear/compose/compose-material/samples", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3-samples", "wear/compose/compose-material3/samples", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-navigation", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-navigation-samples", "wear/compose/compose-navigation/samples", [BuildType.COMPOSE])
includeProject(":wear:compose:integration-tests:demos", [BuildType.COMPOSE])
diff --git a/sqlite/integration-tests/inspection-room-testapp/build.gradle b/sqlite/integration-tests/inspection-room-testapp/build.gradle
index 13b7d0b..a4048b05 100644
--- a/sqlite/integration-tests/inspection-room-testapp/build.gradle
+++ b/sqlite/integration-tests/inspection-room-testapp/build.gradle
@@ -42,7 +42,4 @@
minSdkVersion 26
}
namespace "androidx.sqlite.inspection.roomtestapp"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
diff --git a/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle b/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
index f0e032f..9856bad 100644
--- a/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
+++ b/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
@@ -44,7 +44,4 @@
minSdkVersion 26
}
namespace "androidx.sqlite.inspection.sqldeligttestapp"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 173c1d4..657c4f3 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -46,7 +46,4 @@
android {
namespace "androidx.sqlite.db.framework"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
diff --git a/sqlite/sqlite-inspection/build.gradle b/sqlite/sqlite-inspection/build.gradle
index a6c166d..ba9d20e 100644
--- a/sqlite/sqlite-inspection/build.gradle
+++ b/sqlite/sqlite-inspection/build.gradle
@@ -57,10 +57,3 @@
}
namespace "androidx.sqlite.inspection"
}
-
-// Allow usage of Kotlin's @OptIn.
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
- }
-}
\ No newline at end of file
diff --git a/sqlite/sqlite-ktx/build.gradle b/sqlite/sqlite-ktx/build.gradle
index 32c89ad..f0012cf 100644
--- a/sqlite/sqlite-ktx/build.gradle
+++ b/sqlite/sqlite-ktx/build.gradle
@@ -40,7 +40,4 @@
android {
namespace "androidx.sqlite.db.ktx"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index 47256e3..36053c0 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -33,9 +33,6 @@
android {
namespace "androidx.sqlite.db"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
androidx {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
index 7f8f775..ec91a3f 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
@@ -40,8 +40,19 @@
* @param height pixel height of the display
* @return null if node is null, else a Rect containing visible bounds
*/
- @SuppressWarnings("RectIntersectReturnValueIgnored")
static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
+ return getVisibleBoundsInScreen(node, new Rect(0, 0, width, height));
+ }
+
+ /**
+ * Returns the node's bounds clipped to the size of the display
+ *
+ * @param node
+ * @param displayRect the display rect
+ * @return null if node is null, else a Rect containing visible bounds
+ */
+ @SuppressWarnings("RectIntersectReturnValueIgnored")
+ static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, Rect displayRect) {
if (node == null) {
return null;
}
@@ -49,12 +60,9 @@
Rect nodeRect = new Rect();
node.getBoundsInScreen(nodeRect);
- Rect displayRect = new Rect();
- displayRect.top = 0;
- displayRect.left = 0;
- displayRect.right = width;
- displayRect.bottom = height;
-
+ if (displayRect == null) {
+ displayRect = new Rect();
+ }
nodeRect.intersect(displayRect);
// On platforms that give us access to the node's window
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 8ee007d..04de5ee 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -180,8 +180,7 @@
public @NonNull BySelector descContains(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
- return desc(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring)),
- Pattern.DOTALL));
+ return desc(RegexHelper.getPatternContains(substring));
}
/**
@@ -195,8 +194,7 @@
public @NonNull BySelector descStartsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
- return desc(
- Pattern.compile(String.format("^%s.*$", Pattern.quote(substring)), Pattern.DOTALL));
+ return desc(RegexHelper.getPatternStartsWith(substring));
}
/**
@@ -210,8 +208,7 @@
public @NonNull BySelector descEndsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
- return desc(
- Pattern.compile(String.format("^.*%s$", Pattern.quote(substring)), Pattern.DOTALL));
+ return desc(RegexHelper.getPatternEndsWith(substring));
}
/**
@@ -338,8 +335,7 @@
public @NonNull BySelector textContains(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
- return text(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring)),
- Pattern.DOTALL));
+ return text(RegexHelper.getPatternContains(substring));
}
/**
@@ -353,8 +349,7 @@
public @NonNull BySelector textStartsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
- return text(
- Pattern.compile(String.format("^%s.*$", Pattern.quote(substring)), Pattern.DOTALL));
+ return text(RegexHelper.getPatternStartsWith(substring));
}
/**
@@ -368,8 +363,7 @@
public @NonNull BySelector textEndsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
- return text(
- Pattern.compile(String.format("^.*%s$", Pattern.quote(substring)), Pattern.DOTALL));
+ return text(RegexHelper.getPatternEndsWith(substring));
}
/** Sets the text value criteria for matching. A UI element will be considered a match if its
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/RegexHelper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/RegexHelper.java
new file mode 100644
index 0000000..75c2980
--- /dev/null
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/RegexHelper.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.test.uiautomator;
+
+import androidx.annotation.NonNull;
+
+import java.util.regex.Pattern;
+
+/**
+ * This class contains static helper methods about regex.
+ */
+class RegexHelper {
+
+ private RegexHelper(){}
+
+ /**
+ * Returns a {@link Pattern} that matches when content starts with given string
+ * (case-sensitive).
+ */
+ static Pattern getPatternStartsWith(@NonNull String text) {
+ return Pattern.compile(String.format("^%s.*$", Pattern.quote(text)), Pattern.DOTALL);
+ }
+
+ /**
+ * Returns a {@link Pattern} that matches when content ends with given string
+ * (case-sensitive).
+ */
+ static Pattern getPatternEndsWith(@NonNull String text) {
+ return Pattern.compile(String.format("^.*%s$", Pattern.quote(text)), Pattern.DOTALL);
+ }
+
+ /**
+ * Returns a {@link Pattern} that matches when content contains given string (case-sensitive).
+ */
+ static Pattern getPatternContains(@NonNull String text) {
+ return Pattern.compile(String.format("^.*%s.*$", Pattern.quote(text)), Pattern.DOTALL);
+ }
+}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index cc5a089..86f9e40 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -257,6 +257,8 @@
if (bounds.contains(point.x, point.y)) {
return true;
}
+ Log.d(TAG, String.format("Clipping out-of-bound (%d, %d) into %s.", point.x, point.y,
+ bounds));
point.x = Math.max(bounds.left, Math.min(point.x, bounds.right));
point.y = Math.max(bounds.top, Math.min(point.y, bounds.bottom));
return false;
@@ -265,16 +267,10 @@
/** Returns the visible bounds of a {@code node}. */
@SuppressWarnings("RectIntersectReturnValueIgnored")
private Rect getVisibleBounds(AccessibilityNodeInfo node) {
- // Get the object bounds in screen coordinates
- Rect ret = new Rect();
- node.getBoundsInScreen(ret);
-
- // Trim any portion of the bounds that are not on the screen
+ Rect screen = new Rect();
final int displayId = getDisplayId();
if (displayId == Display.DEFAULT_DISPLAY) {
- final Rect screen =
- new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
- ret.intersect(screen);
+ screen = new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
} else {
final DisplayManager dm =
(DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
@@ -283,21 +279,12 @@
if (display != null) {
final Point size = new Point();
display.getRealSize(size);
- final Rect screen = new Rect(0, 0, size.x, size.y);
- ret.intersect(screen);
+ screen = new Rect(0, 0, size.x, size.y);
+ } else {
+ Log.d(TAG, String.format("Unable to get the display with id %d.", displayId));
}
}
-
- // On platforms that give us access to the node's window
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- // Trim any portion of the bounds that are outside the window
- Rect bounds = new Rect();
- AccessibilityWindowInfo window = Api21Impl.getWindow(node);
- if (window != null) {
- Api21Impl.getBoundsInScreen(window, bounds);
- ret.intersect(bounds);
- }
- }
+ Rect ret = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, screen);
// Find the visible bounds of our first scrollable ancestor
for (AccessibilityNodeInfo ancestor = node.getParent(); ancestor != null;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index 5a6d9b5..fa34a40 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -351,7 +351,7 @@
*/
@NonNull
public static UiObject2Condition<Boolean> descContains(@NonNull String substring) {
- return descMatches(String.format("^.*%s.*$", Pattern.quote(substring)));
+ return descMatches(RegexHelper.getPatternContains(substring));
}
/**
@@ -360,7 +360,7 @@
*/
@NonNull
public static UiObject2Condition<Boolean> descStartsWith(@NonNull String substring) {
- return descMatches(String.format("^%s.*$", Pattern.quote(substring)));
+ return descMatches(RegexHelper.getPatternStartsWith(substring));
}
/**
@@ -369,7 +369,7 @@
*/
@NonNull
public static UiObject2Condition<Boolean> descEndsWith(@NonNull String substring) {
- return descMatches(String.format("^.*%s$", Pattern.quote(substring)));
+ return descMatches(RegexHelper.getPatternEndsWith(substring));
}
/**
@@ -435,7 +435,7 @@
*/
@NonNull
public static UiObject2Condition<Boolean> textContains(@NonNull String substring) {
- return textMatches(String.format("^.*%s.*$", Pattern.quote(substring)));
+ return textMatches(RegexHelper.getPatternContains(substring));
}
/**
@@ -444,7 +444,7 @@
*/
@NonNull
public static UiObject2Condition<Boolean> textStartsWith(@NonNull String substring) {
- return textMatches(String.format("^%s.*$", Pattern.quote(substring)));
+ return textMatches(RegexHelper.getPatternStartsWith(substring));
}
/**
@@ -453,7 +453,7 @@
*/
@NonNull
public static UiObject2Condition<Boolean> textEndsWith(@NonNull String substring) {
- return textMatches(String.format("^.*%s$", Pattern.quote(substring)));
+ return textMatches(RegexHelper.getPatternEndsWith(substring));
}
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
index 4255c21..201f12e 100644
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
+++ b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
@@ -19,6 +19,7 @@
import androidx.testutils.paparazzi.ImageDiffer.DiffResult.Similar
import java.awt.image.BufferedImage
+@JvmDefaultWithCompatibility
/**
* Functional interface to compare two images and returns a [ImageDiffer.DiffResult] ADT containing
* comparison statistics and a difference image, if applicable.
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index 201bb55..6c1c56a 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -38,7 +38,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemsProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
public final class LazyGridKt {
@@ -47,7 +47,7 @@
public final class LazyGridMeasureKt {
}
- public final class LazyGridScrollingKt {
+ public final class LazyGridScrollPositionKt {
}
public final class LazyGridSpanKt {
@@ -169,6 +169,16 @@
}
+package androidx.tv.foundation.lazy.layout {
+
+ public final class LazyAnimateScrollKt {
+ }
+
+ public final class LazyLayoutSemanticsKt {
+ }
+
+}
+
package androidx.tv.foundation.lazy.list {
public final class LazyDslKt {
@@ -186,7 +196,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemsProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -195,7 +205,7 @@
public final class LazyListMeasureKt {
}
- public final class LazyListScrollingKt {
+ public final class LazyListScrollPositionKt {
}
public final class LazyListStateKt {
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index 70eb94426..033c89a 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -42,7 +42,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemsProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
public final class LazyGridKt {
@@ -51,7 +51,7 @@
public final class LazyGridMeasureKt {
}
- public final class LazyGridScrollingKt {
+ public final class LazyGridScrollPositionKt {
}
public final class LazyGridSpanKt {
@@ -175,6 +175,16 @@
}
+package androidx.tv.foundation.lazy.layout {
+
+ public final class LazyAnimateScrollKt {
+ }
+
+ public final class LazyLayoutSemanticsKt {
+ }
+
+}
+
package androidx.tv.foundation.lazy.list {
public final class LazyDslKt {
@@ -192,7 +202,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemsProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -201,7 +211,7 @@
public final class LazyListMeasureKt {
}
- public final class LazyListScrollingKt {
+ public final class LazyListScrollPositionKt {
}
public final class LazyListStateKt {
@@ -253,6 +263,7 @@
@androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ method @androidx.tv.foundation.ExperimentalTvFoundationApi public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
}
@kotlin.DslMarker public @interface TvLazyListScopeMarker {
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index 201bb55..6c1c56a 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -38,7 +38,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemsProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
public final class LazyGridKt {
@@ -47,7 +47,7 @@
public final class LazyGridMeasureKt {
}
- public final class LazyGridScrollingKt {
+ public final class LazyGridScrollPositionKt {
}
public final class LazyGridSpanKt {
@@ -169,6 +169,16 @@
}
+package androidx.tv.foundation.lazy.layout {
+
+ public final class LazyAnimateScrollKt {
+ }
+
+ public final class LazyLayoutSemanticsKt {
+ }
+
+}
+
package androidx.tv.foundation.lazy.list {
public final class LazyDslKt {
@@ -186,7 +196,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemsProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -195,7 +205,7 @@
public final class LazyListMeasureKt {
}
- public final class LazyListScrollingKt {
+ public final class LazyListScrollPositionKt {
}
public final class LazyListStateKt {
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index 29362d9..df16827 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -75,14 +75,6 @@
targetsJavaConsumers = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
-
// Functions and tasks to monitor changes in copied files.
task generateMd5 {
@@ -112,15 +104,15 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/ScrollableWithPivot.kt",
- "afaf0f2be6b57df076db42d9218f83d9"
+ "src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt",
+ "86acc593cd77d52784532163b5ab8156"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/DataIndex.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt",
"2aa3c6d2dd05057478e723b2247517e1"
)
)
@@ -128,15 +120,15 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyItemScopeImpl.kt",
- "31e6796d0d03cb84483396a39fc5b7e7"
+ "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt",
+ "37cb0caf8a170a4161da346806c7a236"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListHeaders.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt",
"4d71c69f9cb38f741da9cfc4109567dd"
)
)
@@ -144,47 +136,39 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt",
"a74bfa05e68e2b6c2e108f022dfbfa26"
)
)
copiedClasses.add(
new CopiedClass(
- "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemProviderImpl.kt",
- "57ff505cbdfa854e15b4fbd9d4a574eb"
- )
-)
-
-copiedClasses.add(
- new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemsProvider.kt",
- "42a2c446c81fba89fd7b8480d063b308"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt",
+ "4c69e8a60a068e1e8191ed3840868881"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyList.kt",
- "c605794683c01c516674436c9ebc1f44"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt",
+ "22078ee2f09dce3f39cdc23dc1188a82"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasure.kt",
- "95c14abd0367f0f39218c9bdd175b242"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt",
+ "c58eaf4619972afbee7da7714dc072fc"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt",
"d4407572c6550d184133f8b3fd37869f"
)
)
@@ -192,63 +176,55 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScopeImpl.kt",
- "1888e8b115c73b5ea7f33d48d9887845"
- )
-)
-
-copiedClasses.add(
- new CopiedClass(
- "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrolling.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrolling.kt",
- "a32b856a1e8740a6a521df04c9d51ed1"
+ "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt",
+ "fab951ddba90c5c5426e4d0104bc9929"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt",
- "82df7d370ba5b20309e5191a0af431a0"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt",
+ "08d08515f25eb3032f6efbf9f86be102"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListState.kt",
- "45821a5bf14d3e6e25fee63e61930f57"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt",
+ "1d16fbb5025b282ffeb8fe3a63a9de3d"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt",
- "78b09b4d78ec9d761274b9ca8d24f4f7"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt",
+ "c1b403d4fcd43c423b3f1b0433e8bb43"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt",
- "bec4211cb3d91bb936e9f0872864244b"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt",
+ "0dcde73635efe26203f70351437cb6fa"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazySemantics.kt",
- "739205f656bf107604ba7167e3cee7e7"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt",
+ "3a1e86a55ea2282c12745717b5a60cfd"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/ItemIndex.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/ItemIndex.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt",
"1031b8b91a81c684b3c4584bc93d3fb0"
)
)
@@ -256,7 +232,7 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt",
"6a0b2db56ef38fb1ac004e4fc9847db8"
)
)
@@ -264,7 +240,7 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemInfo.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemInfo.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt",
"1f3b13ee45de79bc67ace4133e634600"
)
)
@@ -272,15 +248,15 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
- "0bbc162aab675ca2a34350e3044433e7"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
+ "6f93637153ebd05d9cba7ebaf12311c9"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt",
"b3ff4600791c73028b8661c0e2b49110"
)
)
@@ -288,39 +264,31 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScope.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt",
"1a40313cc5e67b5808586c012bbfb058"
)
)
copiedClasses.add(
new CopiedClass(
- "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt",
- "48fdfb1dfa5d39c88d4aa96732192421"
- )
-)
-
-copiedClasses.add(
- new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt",
- "e5e95e6cad43cec2b0c30bf201e3cae9"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt",
+ "ba8ee64efc5bcd18f28fe9bb9d987166"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGrid.kt",
- "69dbab5e83deab809219d4d7a9ee7fa8"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt",
+ "3f91a6975c10c6a49ddc21f7828d7298"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridLayoutInfo.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt",
"b421c5e74856a78982efe0d8a79d10cb"
)
)
@@ -328,15 +296,15 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt",
- "2b38f5261ad092d9048cfc4f0a841a1a"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt",
+ "c600148ddfab1dde9f3ebe8349e77001"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasureResult.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt",
"1277598d36d8507d7bf0305cc629a11c"
)
)
@@ -344,39 +312,31 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeImpl.kt",
- "3296c6edcbd56450ba919df105cb36c0"
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt",
+ "e92ebc01a8b205d304e0b0d3c40636f8"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeMarker.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeMarker.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt",
"0b7ff258a80e2413f89d56ab0ef41b46"
)
)
copiedClasses.add(
new CopiedClass(
- "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt",
- "15f2f9bb89c1603aa4b7e7d1f8a2de5a"
- )
-)
-
-copiedClasses.add(
- new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt",
- "9b3d47322ad526fb17a3d9505a80f673"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt",
+ "70bac76aeb2617b8f5c706f1867800fd"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt",
"cc63cb4f05cc556e8fcf7504ac0ea57c"
)
)
@@ -384,71 +344,71 @@
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
- "894b9f69a27e247bbe609bdac22bb5ed"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
+ "062f95aa00d36fb1e048aa1ddb8154bc"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridState.kt",
- "1e37d8a6f159aabe11f488121de59b70"
+ "src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt",
+ "c6b402b685824ff216650da77063a131"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt",
- "09d9b21d33325a94cac738aad58e2422"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt",
+ "b9e6230825d8688bf1164abef07b4e14"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
- "3acdfddfd06eb17aac5dbdd326482e35"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
+ "ab9a58f65e85b4fe4d621e9ed5b2db68"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt",
- "1104f01e8b1f6eced2401b207114f4a4"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt",
+ "3b99751e25cebc9945df800ce1aa04f8"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
- "b7b731e6e8fdc520064aaef989575bda"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
+ "e2bdba6cdbc870ea9607658ec60eb1eb"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazySemantics.kt",
- "dab277484b4ec57a5275095b505f79d4"
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt",
+ "bb397307f2cc3fd87bcc7585bf403039"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyDsl.kt",
- "8462c0a61f14639f39dd6f76c6a2aebc"
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt",
+ "9d86fad30c0f3de2231fbef3f63db53e"
)
)
copiedClasses.add(
new CopiedClass(
"../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinningModifier.kt",
- "src/commonMain/kotlin/androidx/tv/foundation/lazy/LazyListPinningModifier.kt",
+ "src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt",
"e37450505d13ab0fd1833f136ec8aa3c"
)
)
@@ -485,36 +445,71 @@
)
)
-task doCopiesNeedUpdate {
- ext.genMd5 = { fileNameToHash ->
- try {
- MessageDigest digest = MessageDigest.getInstance("MD5")
- file(fileNameToHash).withInputStream() { is ->
- byte[] buffer = new byte[8192]
- int read
- while ((read = is.read(buffer)) > 0) {
- digest.update(buffer, 0, read);
- }
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt",
+ "src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt",
+ "8b9e4a03c5097b4ef7377f98da95bbcd"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt",
+ "src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt",
+ "72859815545394de5b9f7269f1366d21"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt",
+ "src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt",
+ "315f220a2674a50f82633a725dc39c1b"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt",
+ "d0d48557af324db3af7f4c46a6810026"
+ )
+)
+
+String generateMd5(String fileNameToHash) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("MD5")
+ file(fileNameToHash).withInputStream() { is ->
+ byte[] buffer = new byte[8192]
+ int read
+ while ((read = is.read(buffer)) > 0) {
+ digest.update(buffer, 0, read);
}
- byte[] md5sum = digest.digest()
- BigInteger bigInt = new BigInteger(1, md5sum)
- bigInt.toString(16).padLeft(32, '0')
- } catch (Exception e) {
- throw new GradleException("Failed for file=$fileNameToHash", e)
}
+ byte[] md5sum = digest.digest()
+ BigInteger bigInt = new BigInteger(1, md5sum)
+ bigInt.toString(16).padLeft(32, '0')
+ } catch (Exception e) {
+ throw new GradleException("Failed for file=$fileNameToHash", e)
}
+}
-
+task doCopiesNeedUpdate {
doLast {
List<String> failureFiles = new ArrayList<>()
+ boolean failures = false
copiedClasses.forEach(copiedClass -> {
+ assert file(copiedClass.originalFilePath).exists()
+ assert file(copiedClass.copyFilePath).exists()
try {
- String actualMd5 = genMd5(copiedClass.originalFilePath)
+ String actualMd5 = generateMd5(copiedClass.originalFilePath)
if (copiedClass.lastKnownGoodHash != actualMd5) {
- failureFiles.add(copiedClass.toString()+ ", actual=" + actualMd5)
+ failureFiles.add(copiedClass.toString()+ "\nactual= " + actualMd5 + "\n")
}
} catch (Exception e) {
- throw new GradleException("Failed for file=${copiedClass.originalFilePath}", e)
+ logger.error("Failed for file=${copiedClass.originalFilePath}", e)
+ failures = true
}
})
@@ -523,7 +518,11 @@
"Files that were copied have been updated at the source. " +
"Please update the copy and then" +
" update the hash in the compose-foundation build.gradle file." +
- failureFiles.stream().collect(Collectors.joining("\n", "\n", "")))
+ failureFiles.stream().collect(Collectors.joining("\n", "\n", "")) + "\ncount=${failureFiles.size()}")
+ }
+
+ if (failures) {
+ throw new GradleException("There were errors. Check the logs.")
}
}
}
@@ -541,8 +540,9 @@
@Override
String toString() {
- return "originalFilePath='" + originalFilePath + '\'' +
- ", copyFilePath='" + copyFilePath + '\'' +
- ", lastKnownGoodHash='" + lastKnownGoodHash + '\''
+ return "originalFilePath='" + originalFilePath + '\'\n' +
+ "copyFilePath='" + copyFilePath + '\'\n' +
+ "lastKnownGoodHash='" + lastKnownGoodHash + '\'\n' +
+ "diffCmd='" + "kdiff3 " + originalFilePath + " " + copyFilePath + "\'"
}
}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
index 3f108d0..a7bed08 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -49,6 +49,7 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.IterableSubject
import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -147,6 +148,7 @@
)
}
+ @Ignore // b/242180919
@Test
fun verifyScrollActionsAtEnd() {
createScrollableContent_StartAtEnd()
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
index b14edf9..7c3174f 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
@@ -38,6 +38,7 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -250,6 +251,7 @@
assertThat(state.canScrollBackward).isFalse()
}
+ @Ignore("b/259608530")
@Test
fun canScrollBackward() = runBlocking {
withContext(Dispatchers.Main + AutoTestFrameClock()) {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt
new file mode 100644
index 0000000..fa613db
--- /dev/null
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.ExperimentalTvFoundationApi
+import androidx.tv.foundation.PivotOffsets
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTvFoundationApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyListHeadersTest {
+
+ private val TvLazyListTag = "TvLazyList"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun tvLazyColumnShowsHeader_withoutBeyondBoundsItemCount() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContent {
+ TvLazyColumn(Modifier.height(300.dp), beyondBoundsItemCount = 0) {
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it))
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag(secondHeaderTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun tvLazyColumnPlaceSecondHeader_ifBeyondBoundsItemCountIsUsed() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContent {
+ TvLazyColumn(Modifier.height(300.dp), beyondBoundsItemCount = 1) {
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it))
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag(secondHeaderTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertExists()
+ }
+
+ @Test
+ fun tvLazyColumnShowsHeadersOnScroll() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ lateinit var state: TvLazyListState
+
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.height(300.dp).testTag(TvLazyListTag).border(2.dp, Color.Black),
+ rememberTvLazyListState().also { state = it }
+ ) {
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth().border(2.dp, Color.Green)
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.height(101.dp).fillParentMaxWidth().border(2.dp, Color.Blue)
+ .testTag(it))
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth().border(2.dp, Color.Yellow)
+ .testTag(secondHeaderTag)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.runOnIdle {
+ assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+ assertEquals(0, state.layoutInfo.visibleItemsInfo.first().offset)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun tvLazyColumnHeaderIsReplaced() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ lateinit var state: TvLazyListState
+
+ rule.setContentWithTestViewConfiguration {
+ state = rememberTvLazyListState()
+ TvLazyColumn(
+ modifier = Modifier.height(300.dp).testTag(TvLazyListTag),
+ state = state
+ ) {
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag(secondHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it))
+ }
+ }
+ }
+
+ rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun tvLazyRowShowsHeader_withoutOffscreenItens() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContent {
+ TvLazyRow(Modifier.width(300.dp), beyondBoundsItemCount = 0) {
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(secondHeaderTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun tvLazyRowPlaceSecondHeader_ifBeyondBoundsItemCountIsUsed() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContent {
+ TvLazyRow(Modifier.width(300.dp), beyondBoundsItemCount = 1) {
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(secondHeaderTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertExists()
+ }
+
+ @Test
+ fun tvLazyRowShowsHeadersOnScroll() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ lateinit var state: TvLazyListState
+
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ Modifier.width(300.dp).testTag(TvLazyListTag),
+ rememberTvLazyListState().also { state = it }
+ ) {
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(secondHeaderTag)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+ rule.onNodeWithTag(TvLazyListTag)
+ .scrollBy(x = 102.dp, density = rule.density)
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.runOnIdle {
+ assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+ assertEquals(0, state.layoutInfo.visibleItemsInfo.first().offset)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun tvLazyRowHeaderIsReplaced() {
+ val items = (1..2).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ lateinit var state: TvLazyListState
+
+ rule.setContentWithTestViewConfiguration {
+ state = rememberTvLazyListState()
+ TvLazyRow(modifier = Modifier.width(300.dp).testTag(TvLazyListTag), state = state) {
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(firstHeaderTag)
+ )
+ }
+
+ stickyHeader {
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag(secondHeaderTag)
+ )
+ }
+
+ items(items) {
+ Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it))
+ }
+ }
+ }
+
+ rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
+
+ rule.onNodeWithTag(firstHeaderTag)
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun headerIsDisplayedWhenItIsFullyInContentPadding() {
+ val headerTag = "header"
+ val itemIndexPx = 100
+ val itemIndexDp = with(rule.density) { itemIndexPx.toDp() }
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ TvLazyColumn(
+ Modifier.requiredSize(itemIndexDp * 4),
+ state = rememberTvLazyListState().also { state = it },
+ contentPadding = PaddingValues(top = itemIndexDp * 2)
+ ) {
+ stickyHeader {
+ Spacer(Modifier.requiredSize(itemIndexDp).testTag(headerTag))
+ }
+
+ items((0..4).toList()) {
+ Spacer(Modifier.requiredSize(itemIndexDp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking { state.scrollToItem(1, itemIndexPx / 2) }
+ }
+
+ rule.onNodeWithTag(headerTag)
+ .assertTopPositionInRootIsEqualTo(itemIndexDp / 2)
+
+ rule.runOnIdle {
+ assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+ assertEquals(
+ itemIndexPx / 2 - /* content padding size */ itemIndexPx * 2,
+ state.layoutInfo.visibleItemsInfo.first().offset
+ )
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemIndexDp * 3 / 2)
+ }
+}
+
+@Composable
+private fun TvLazyColumn(
+ modifier: Modifier = Modifier,
+ state: TvLazyListState = rememberTvLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ userScrollEnabled: Boolean = true,
+ beyondBoundsItemCount: Int,
+ content: TvLazyListScope.() -> Unit
+) {
+ LazyList(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ horizontalAlignment = horizontalAlignment,
+ verticalArrangement = verticalArrangement,
+ isVertical = true,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ beyondBoundsItemCount = beyondBoundsItemCount,
+ content = content,
+ pivotOffsets = PivotOffsets()
+ )
+}
+
+@Composable
+private fun TvLazyRow(
+ modifier: Modifier = Modifier,
+ state: TvLazyListState = rememberTvLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ userScrollEnabled: Boolean = true,
+ beyondBoundsItemCount: Int,
+ content: TvLazyListScope.() -> Unit
+) {
+ LazyList(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ isVertical = false,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ beyondBoundsItemCount = beyondBoundsItemCount,
+ content = content,
+ pivotOffsets = PivotOffsets()
+ )
+}
+
+internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
+ performTouchInput {
+ with(density) {
+ val touchSlop = TestTouchSlop.toInt()
+ val xPx = x.roundToPx()
+ val yPx = y.roundToPx()
+ val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
+ val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
+ swipeWithVelocity(
+ start = center,
+ end = Offset(center.x - offsetX, center.y - offsetY),
+ endVelocity = 0f
+ )
+ }
+ }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
index edc6150..9f0c871 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
@@ -40,14 +40,20 @@
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.OnPlacedModifier
import androidx.compose.ui.layout.OnRemeasuredModifier
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.util.fastForEach
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -111,6 +117,20 @@
}
)
+internal interface ScrollConfig {
+ fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset
+}
+
+@Composable
+internal fun platformScrollConfig(): ScrollConfig = AndroidConfig
+
+private object AndroidConfig : ScrollConfig {
+ override fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset {
+ // 64 dp value is taken from ViewConfiguration.java, replace with better solution
+ return event.changes.fastFold(Offset.Zero) { acc, c -> acc + c.scrollDelta } * -64.dp.toPx()
+ }
+}
+
@Suppress("ComposableModifierFactory")
@Composable
private fun Modifier.pointerScrollable(
@@ -325,3 +345,26 @@
override val key = ModifierLocalScrollableContainer
override val value = true
}
+
+/**
+ * Accumulates value starting with [initial] value and applying [operation] from left to right
+ * to current accumulator value and each element.
+ *
+ * Returns the specified [initial] value if the collection is empty.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ *
+ * @param [operation] function that takes current accumulator value and an element, and calculates the next accumulator value.
+ */
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T, R> List<T>.fastFold(initial: R, operation: (acc: R, T) -> R): R {
+ contract { callsInPlace(operation) }
+ var accumulator = initial
+ fastForEach { e ->
+ accumulator = operation(accumulator, e)
+ }
+ return accumulator
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
index 82f9608..cecf3d1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.checkScrollableContainerConstraints
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
@@ -36,7 +37,6 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
@@ -44,6 +44,7 @@
import androidx.compose.ui.util.fastForEach
import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.layout.lazyLayoutSemantics
import androidx.tv.foundation.scrollableWithPivot
@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@@ -74,7 +75,9 @@
/** The content of the grid */
content: TvLazyGridScope.() -> Unit
) {
- val itemProvider = rememberItemProvider(state, content)
+ val itemProvider = rememberLazyGridItemProvider(state, content)
+
+ val semanticState = rememberLazyGridSemanticState(state, itemProvider, reverseLayout)
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
@@ -103,28 +106,20 @@
modifier = modifier
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
- .lazyGridSemantics(
+ .lazyLayoutSemantics(
itemProvider = itemProvider,
- state = state,
- coroutineScope = scope,
- isVertical = isVertical,
- reverseScrolling = reverseLayout,
+ state = semanticState,
+ orientation = orientation,
userScrollEnabled = userScrollEnabled
)
.clipScrollableContainer(orientation)
.scrollableWithPivot(
orientation = orientation,
- reverseDirection = run {
- // A finger moves with the content, not with the viewport. Therefore,
- // always reverse once to have "natural" gesture that goes reversed to layout
- var reverseDirection = !reverseLayout
- // But if rtl and horizontal, things move the other way around
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- if (isRtl && !isVertical) {
- reverseDirection = !reverseDirection
- }
- reverseDirection
- },
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseLayout
+ ),
state = state,
enabled = userScrollEnabled,
pivotOffsets = pivotOffsets
@@ -332,7 +327,6 @@
measuredLineProvider = measuredLineProvider,
measuredItemProvider = measuredItemProvider,
mainAxisAvailableSize = mainAxisAvailableSize,
- slotsPerLine = resolvedSlotSizesSums.size,
beforeContentPadding = beforeContentPadding,
afterContentPadding = afterContentPadding,
spaceBetweenLines = spaceBetweenLines,
@@ -346,6 +340,7 @@
reverseLayout = reverseLayout,
density = this,
placementAnimator = placementAnimator,
+ spanLayoutProvider = itemProvider.spanLayoutProvider,
layout = { width, height, placement ->
layout(
containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
new file mode 100644
index 0000000..6584f8f
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2022 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.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
+import kotlin.math.max
+
+internal class LazyGridAnimateScrollScope(
+ private val state: TvLazyGridState
+) : LazyAnimateScrollScope {
+ override val density: Density get() = state.density
+
+ override val firstVisibleItemIndex: Int get() = state.firstVisibleItemIndex
+
+ override val firstVisibleItemScrollOffset: Int get() = state.firstVisibleItemScrollOffset
+
+ override val lastVisibleItemIndex: Int
+ get() = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+
+ override val itemCount: Int get() = state.layoutInfo.totalItemsCount
+
+ override fun getTargetItemOffset(index: Int): Int? =
+ state.layoutInfo.visibleItemsInfo
+ .fastFirstOrNull {
+ it.index == index
+ }?.let { item ->
+ if (state.isVertical) {
+ item.offset.y
+ } else {
+ item.offset.x
+ }
+ }
+
+ override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
+ state.snapToItemIndexInternal(index, scrollOffset)
+ }
+
+ override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
+ val visibleItems = state.layoutInfo.visibleItemsInfo
+ val slotsPerLine = state.slotsPerLine
+ val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
+ visibleItems,
+ state.isVertical
+ )
+ val before = index < firstVisibleItemIndex
+ val linesDiff =
+ (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
+ slotsPerLine
+
+ return (averageLineMainAxisSize * linesDiff).toFloat() +
+ targetScrollOffset - firstVisibleItemScrollOffset
+ }
+
+ override val numOfItemsForTeleport: Int get() = 100 * state.slotsPerLine
+
+ private fun calculateLineAverageMainAxisSize(
+ visibleItems: List<TvLazyGridItemInfo>,
+ isVertical: Boolean
+ ): Int {
+ val lineOf: (Int) -> Int = {
+ if (isVertical) visibleItems[it].row else visibleItems[it].column
+ }
+
+ var totalLinesMainAxisSize = 0
+ var linesCount = 0
+
+ var lineStartIndex = 0
+ while (lineStartIndex < visibleItems.size) {
+ val currentLine = lineOf(lineStartIndex)
+ if (currentLine == -1) {
+ // Filter out exiting items.
+ ++lineStartIndex
+ continue
+ }
+
+ var lineMainAxisSize = 0
+ var lineEndIndex = lineStartIndex
+ while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
+ lineMainAxisSize = max(
+ lineMainAxisSize,
+ if (isVertical) {
+ visibleItems[lineEndIndex].size.height
+ } else {
+ visibleItems[lineEndIndex].size.width
+ }
+ )
+ ++lineEndIndex
+ }
+
+ totalLinesMainAxisSize += lineMainAxisSize
+ ++linesCount
+
+ lineStartIndex = lineEndIndex
+ }
+
+ return totalLinesMainAxisSize / linesCount
+ }
+
+ override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
+ state.scroll(block = block)
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 6db01c4..b050f5e 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -31,7 +31,6 @@
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
-import kotlin.math.absoluteValue
import kotlin.math.max
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -50,8 +49,6 @@
private val scope: CoroutineScope,
private val isVertical: Boolean
) {
- private var slotsPerLine = 0
-
// state containing an animation and all relevant info for each item.
private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
@@ -76,10 +73,10 @@
consumedScroll: Int,
layoutWidth: Int,
layoutHeight: Int,
- slotsPerLine: Int,
reverseLayout: Boolean,
positionedItems: MutableList<TvLazyGridPositionedItem>,
measuredItemProvider: LazyMeasuredItemProvider,
+ spanLayoutProvider: LazyGridSpanLayoutProvider
) {
if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
// no animations specified - no work needed
@@ -87,8 +84,6 @@
return
}
- this.slotsPerLine = slotsPerLine
-
val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
// the consumed scroll is considered as a delta we don't need to animate
@@ -172,7 +167,9 @@
scrolledBy = notAnimatableDelta,
fallback = fallback,
reverseLayout = reverseLayout,
- mainAxisLayoutSize = mainAxisLayoutSize
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ visibleItems = positionedItems,
+ spanLayoutProvider = spanLayoutProvider
)
}
val targetPlaceableOffset = if (isVertical) {
@@ -274,7 +271,9 @@
scrolledBy = notAnimatableDelta,
fallback = mainAxisLayoutSize,
reverseLayout = reverseLayout,
- mainAxisLayoutSize = mainAxisLayoutSize
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ visibleItems = positionedItems,
+ spanLayoutProvider = spanLayoutProvider
)
val targetOffset = if (reverseLayout) {
mainAxisLayoutSize - absoluteTargetOffset - measuredItem.mainAxisSize
@@ -355,27 +354,48 @@
scrolledBy: IntOffset,
reverseLayout: Boolean,
mainAxisLayoutSize: Int,
- fallback: Int
+ fallback: Int,
+ visibleItems: List<TvLazyGridPositionedItem>,
+ spanLayoutProvider: LazyGridSpanLayoutProvider
): Int {
- require(slotsPerLine != 0)
- val beforeViewportStart =
- if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
val afterViewportEnd =
+ if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+ val beforeViewportStart =
if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
return when {
- beforeViewportStart -> {
- val diff = ((index - viewportEndItemIndex).absoluteValue + slotsPerLine - 1) /
- slotsPerLine
- mainAxisLayoutSize + viewportEndItemNotVisiblePartSize +
- averageLineMainAxisSize * (diff - 1) +
- scrolledBy.mainAxis
- }
afterViewportEnd -> {
- val diff = ((viewportStartItemIndex - index).absoluteValue + slotsPerLine - 1) /
- slotsPerLine
- viewportStartItemNotVisiblePartSize - mainAxisSizeWithSpacings -
- averageLineMainAxisSize * (diff - 1) +
- scrolledBy.mainAxis
+ val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
+ if (!reverseLayout) viewportEndItemIndex else index
+ )
+ val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
+ if (!reverseLayout) index else viewportEndItemIndex
+ )
+ mainAxisLayoutSize + viewportEndItemNotVisiblePartSize + scrolledBy.mainAxis +
+ // add sizes of the lines between the last visible one and this one.
+ spanLayoutProvider.getLinesMainAxisSizesSum(
+ fromIndex = fromIndex,
+ toIndex = toIndex,
+ averageLineMainAxisSize = averageLineMainAxisSize,
+ visibleItems = visibleItems
+ )
+ }
+ beforeViewportStart -> {
+ val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
+ if (!reverseLayout) index else viewportStartItemIndex
+ )
+ val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
+ if (!reverseLayout) viewportStartItemIndex else index
+ )
+ viewportStartItemNotVisiblePartSize + scrolledBy.mainAxis +
+ // minus the size of this item as we are looking for the start offset of it.
+ -mainAxisSizeWithSpacings +
+ // minus sizes of the lines between the first visible one and this one.
+ -spanLayoutProvider.getLinesMainAxisSizesSum(
+ fromIndex = fromIndex,
+ toIndex = toIndex,
+ averageLineMainAxisSize = averageLineMainAxisSize,
+ visibleItems = visibleItems
+ )
}
else -> {
fallback
@@ -461,3 +481,81 @@
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
)
+
+private fun LazyGridSpanLayoutProvider.getLinesMainAxisSizesSum(
+ fromIndex: Int,
+ toIndex: Int,
+ averageLineMainAxisSize: Int,
+ visibleItems: List<TvLazyGridPositionedItem>
+): Int {
+ var index = fromIndex
+ var sizes = 0
+ while (index <= toIndex) {
+ val lastItemInTheLine = firstIndexInNextLineAfter(index) - 1
+ if (lastItemInTheLine <= toIndex) {
+ sizes += visibleItems.getLineSize(lastItemInTheLine, averageLineMainAxisSize)
+ }
+ index = lastItemInTheLine + 1
+ }
+ return sizes
+}
+
+private fun List<TvLazyGridPositionedItem>.getLineSize(itemIndex: Int, fallback: Int): Int {
+ if (isEmpty() || itemIndex < first().index || itemIndex > last().index) return fallback
+ if ((itemIndex - first().index) < (last().index - itemIndex)) {
+ for (index in indices) {
+ val item = get(index)
+ if (item.index == itemIndex) return item.lineMainAxisSizeWithSpacings
+ if (item.index > itemIndex) break
+ }
+ } else {
+ for (index in lastIndex downTo 0) {
+ val item = get(index)
+ if (item.index == itemIndex) return item.lineMainAxisSizeWithSpacings
+ if (item.index < itemIndex) break
+ }
+ }
+ return fallback
+}
+
+private fun LazyGridSpanLayoutProvider.lastIndexInPreviousLineBefore(index: Int) =
+ firstIndexInLineContaining(index) - 1
+
+private fun LazyGridSpanLayoutProvider.firstIndexInNextLineAfter(index: Int) =
+ if (index >= totalSize) {
+ // after totalSize we just approximate with 1 slot per item
+ firstIndexInLineContaining(index) + slotsPerLine
+ } else {
+ val lineIndex = getLineIndexOfItem(index)
+ val lineConfiguration = getLineConfiguration(lineIndex.value)
+ lineConfiguration.firstItemIndex + lineConfiguration.spans.size
+ }
+
+private fun LazyGridSpanLayoutProvider.firstIndexInLineContaining(index: Int): Int {
+ return if (index >= totalSize) {
+ val firstIndexForLastKnowLine = getFirstIndexInNextLineAfterTheLastKnownOne()
+ // after totalSize we just approximate with 1 slot per item
+ val linesBetween = (index - firstIndexForLastKnowLine) / slotsPerLine
+ firstIndexForLastKnowLine + slotsPerLine * linesBetween
+ } else {
+ val lineIndex = getLineIndexOfItem(index)
+ val lineConfiguration = getLineConfiguration(lineIndex.value)
+ lineConfiguration.firstItemIndex
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyGridSpanLayoutProvider.getFirstIndexInNextLineAfterTheLastKnownOne(): Int {
+ // first we find the line for the `totalSize - 1` item
+ val lineConfiguration = getLineConfiguration(getLineIndexOfItem(totalSize - 1).value)
+ var currentSpan = 0
+ var currentIndex = lineConfiguration.firstItemIndex - 1
+ // then we go through all the known spans
+ lineConfiguration.spans.fastForEach {
+ currentSpan += it.currentLineSpan
+ currentIndex++
+ }
+ // and increment index as if we had more items with slot == 1 until we switch to the next line
+ currentIndex += slotsPerLine - currentSpan + 1
+ return currentIndex
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
index bfab189..c77504d 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -17,10 +17,95 @@
package androidx.tv.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
internal interface LazyGridItemProvider : LazyLayoutItemProvider {
val spanLayoutProvider: LazyGridSpanLayoutProvider
+ val hasCustomSpans: Boolean
+
+ fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan
}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyGridItemProvider(
+ state: TvLazyGridState,
+ content: TvLazyGridScope.() -> Unit,
+): LazyGridItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = remember(state) {
+ { state.firstVisibleItemIndex }
+ },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount }
+ )
+
+ return remember(nearestItemsRangeState) {
+ val itemProviderState: State<LazyGridItemProvider> = derivedStateOf {
+ val gridScope = TvLazyGridScopeImpl().apply(latestContent.value)
+ LazyGridItemProviderImpl(
+ gridScope.intervals,
+ gridScope.hasCustomSpans,
+ nearestItemsRangeState.value
+ )
+ }
+
+ object : LazyGridItemProvider,
+ LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+ override val spanLayoutProvider: LazyGridSpanLayoutProvider
+ get() = itemProviderState.value.spanLayoutProvider
+
+ override val hasCustomSpans: Boolean
+ get() = itemProviderState.value.hasCustomSpans
+
+ override fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan =
+ with(itemProviderState.value) {
+ getSpan(index)
+ }
+ }
+ }
+}
+
+@ExperimentalFoundationApi
+private class LazyGridItemProviderImpl(
+ private val intervals: IntervalList<LazyGridIntervalContent>,
+ override val hasCustomSpans: Boolean,
+ nearestItemsRange: IntRange
+) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
+ intervals = intervals,
+ nearestItemsRange = nearestItemsRange,
+ itemContent = { interval, index ->
+ interval.item.invoke(TvLazyGridItemScopeImpl, index)
+ }
+) {
+ override val spanLayoutProvider: LazyGridSpanLayoutProvider =
+ LazyGridSpanLayoutProvider(this)
+
+ override fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan {
+ val interval = intervals[index]
+ val localIntervalIndex = index - interval.startIndex
+ return interval.value.span.invoke(this, localIntervalIndex)
+ }
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
deleted file mode 100644
index 9ee4e5e..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
- state: TvLazyGridState,
- content: TvLazyGridScope.() -> Unit,
-): LazyGridItemProvider {
- val latestContent = rememberUpdatedState(content)
- // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
- // of derivedState in return expr will only happen after the state value has been changed.
- val nearestItemsRangeState = remember(state) {
- mutableStateOf(
- Snapshot.withoutReadObservation {
- // State read is observed in composition, causing it to recompose 1 additional time.
- calculateNearestItemsRange(state.firstVisibleItemIndex)
- }
- )
- }
- LaunchedEffect(nearestItemsRangeState) {
- snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
- // MutableState's SnapshotMutationPolicy will make sure the provider is only
- // recreated when the state is updated with a new range.
- .collect { nearestItemsRangeState.value = it }
- }
- return remember(nearestItemsRangeState) {
- LazyGridItemProviderImpl(
- derivedStateOf {
- val listScope = TvLazyGridScopeImpl().apply(latestContent.value)
- LazyGridItemsSnapshot(
- listScope.intervals,
- listScope.hasCustomSpans,
- nearestItemsRangeState.value
- )
- }
- )
- }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyGridItemsSnapshot(
- private val intervals: IntervalList<LazyGridIntervalContent>,
- val hasCustomSpans: Boolean,
- nearestItemsRange: IntRange
-) {
- val itemsCount get() = intervals.size
-
- val spanLayoutProvider = LazyGridSpanLayoutProvider(this)
-
- fun getKey(index: Int): Any {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- val key = interval.value.key?.invoke(localIntervalIndex)
- return key ?: getDefaultLazyLayoutKey(index)
- }
-
- fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- return interval.value.span.invoke(this, localIntervalIndex)
- }
-
- @Composable
- fun Item(index: Int) {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- interval.value.item.invoke(TvLazyGridItemScopeImpl, localIntervalIndex)
- }
-
- val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
- fun getContentType(index: Int): Any? {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- return interval.value.type.invoke(localIntervalIndex)
- }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyGridItemProviderImpl(
- private val itemsSnapshot: State<LazyGridItemsSnapshot>
-) : LazyGridItemProvider {
-
- override val itemCount get() = itemsSnapshot.value.itemsCount
-
- override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
- @Composable
- override fun Item(index: Int) {
- itemsSnapshot.value.Item(index)
- }
-
- override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
- override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-
- override val spanLayoutProvider: LazyGridSpanLayoutProvider
- get() = itemsSnapshot.value.spanLayoutProvider
-}
-
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
- range: IntRange,
- list: IntervalList<LazyGridIntervalContent>
-): Map<Any, Int> {
- val first = range.first
- check(first >= 0)
- val last = minOf(range.last, list.size - 1)
- return if (last < first) {
- emptyMap()
- } else {
- hashMapOf<Any, Int>().also { map ->
- list.forEach(
- fromIndex = first,
- toIndex = last,
- ) {
- if (it.value.key != null) {
- val keyFactory = requireNotNull(it.value.key)
- val start = maxOf(first, it.startIndex)
- val end = minOf(last, it.startIndex + it.size - 1)
- for (i in start..end) {
- map[keyFactory(i - it.startIndex)] = i
- }
- }
- }
- }
- }
-}
-
-/**
- * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
- val slidingWindowStart = VisibleItemsSlidingWindowSize *
- (firstVisibleItem / VisibleItemsSlidingWindowSize)
-
- val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
- val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
- return start until end
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private val VisibleItemsSlidingWindowSize = 90
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private val ExtraItemsNearTheSlidingWindow = 200
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index 03003be..d599225 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -41,7 +41,6 @@
measuredLineProvider: LazyMeasuredLineProvider,
measuredItemProvider: LazyMeasuredItemProvider,
mainAxisAvailableSize: Int,
- slotsPerLine: Int,
beforeContentPadding: Int,
afterContentPadding: Int,
spaceBetweenLines: Int,
@@ -55,6 +54,7 @@
reverseLayout: Boolean,
density: Density,
placementAnimator: LazyGridItemPlacementAnimator,
+ spanLayoutProvider: LazyGridSpanLayoutProvider,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): TvLazyGridMeasureResult {
require(beforeContentPadding >= 0)
@@ -245,10 +245,10 @@
consumedScroll = consumedScroll.toInt(),
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
- slotsPerLine = slotsPerLine,
reverseLayout = reverseLayout,
positionedItems = positionedItems,
- measuredItemProvider = measuredItemProvider
+ measuredItemProvider = measuredItemProvider,
+ spanLayoutProvider = spanLayoutProvider
)
return TvLazyGridMeasureResult(
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
index c7a6165..1dbbbf0 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -92,7 +92,10 @@
*/
fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
Snapshot.withoutReadObservation {
- update(findLazyGridIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+ update(
+ ItemIndex(itemProvider.findIndexByKey(lastKnownFirstItemKey, index.value)),
+ scrollOffset
+ )
}
}
@@ -105,33 +108,31 @@
this.scrollOffset = scrollOffset
}
}
+}
- private companion object {
- /**
- * Finds a position of the item with the given key in the grid. This logic allows us to
- * detect when there were items added or removed before our current first item.
- */
- private fun findLazyGridIndexByKey(
- key: Any?,
- lastKnownIndex: ItemIndex,
- itemProvider: LazyGridItemProvider
- ): ItemIndex {
- if (key == null) {
- // there were no real item during the previous measure
- return lastKnownIndex
- }
- if (lastKnownIndex.value < itemProvider.itemCount &&
- key == itemProvider.getKey(lastKnownIndex.value)
- ) {
- // this item is still at the same index
- return lastKnownIndex
- }
- val newIndex = itemProvider.keyToIndexMap[key]
- if (newIndex != null) {
- return ItemIndex(newIndex)
- }
- // fallback to the previous index if we don't know the new index of the item
- return lastKnownIndex
- }
+/**
+ * Finds a position of the item with the given key in the lists. This logic allows us to
+ * detect when there were items added or removed before our current first item.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal fun LazyGridItemProvider.findIndexByKey(
+ key: Any?,
+ lastKnownIndex: Int,
+): Int {
+ if (key == null) {
+ // there were no real item during the previous measure
+ return lastKnownIndex
}
+ if (lastKnownIndex < itemCount &&
+ key == getKey(lastKnownIndex)
+ ) {
+ // this item is still at the same index
+ return lastKnownIndex
+ }
+ val newIndex = keyToIndexMap[key]
+ if (newIndex != null) {
+ return newIndex
+ }
+ // fallback to the previous index if we don't know the new index of the item
+ return lastKnownIndex
}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
deleted file mode 100644
index 20bbd68..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.copy
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFirstOrNull
-import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.abs
-import kotlin.math.max
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@OptIn(ExperimentalFoundationApi::class)
-private class ItemFoundInScroll(
- val item: TvLazyGridItemInfo,
- val previousAnimation: AnimationState<Float, AnimationVector1D>
-) : CancellationException()
-
-private val TargetDistance = 2500.dp
-private val BoundDistance = 1500.dp
-
-private const val DEBUG = false
-private inline fun debugLog(generateMsg: () -> String) {
- if (DEBUG) {
- println("LazyGridScrolling: ${generateMsg()}")
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal suspend fun TvLazyGridState.doSmoothScrollToItem(
- index: Int,
- scrollOffset: Int,
- slotsPerLine: Int
-) {
- require(index >= 0f) { "Index should be non-negative ($index)" }
- fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
- it.index == index
- }
- scroll {
- try {
- val targetDistancePx = with(density) { TargetDistance.toPx() }
- val boundDistancePx = with(density) { BoundDistance.toPx() }
- var loop = true
- var anim = AnimationState(0f)
- val targetItemInitialInfo = getTargetItem()
- if (targetItemInitialInfo != null) {
- // It's already visible, just animate directly
- throw ItemFoundInScroll(
- targetItemInitialInfo,
- anim
- )
- }
- val forward = index > firstVisibleItemIndex
-
- fun isOvershot(): Boolean {
- // Did we scroll past the item?
- @Suppress("RedundantIf") // It's way easier to understand the logic this way
- return if (forward) {
- if (firstVisibleItemIndex > index) {
- true
- } else if (
- firstVisibleItemIndex == index &&
- firstVisibleItemScrollOffset > scrollOffset
- ) {
- true
- } else {
- false
- }
- } else { // backward
- if (firstVisibleItemIndex < index) {
- true
- } else if (
- firstVisibleItemIndex == index &&
- firstVisibleItemScrollOffset < scrollOffset
- ) {
- true
- } else {
- false
- }
- }
- }
-
- var loops = 1
- while (loop && layoutInfo.totalItemsCount > 0) {
- val visibleItems = layoutInfo.visibleItemsInfo
- val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
- visibleItems,
- true // TODO(b/191238807)
- )
- val before = index < firstVisibleItemIndex
- val linesDiff =
- (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
- slotsPerLine
-
- val expectedDistance = (averageLineMainAxisSize * linesDiff).toFloat() +
- scrollOffset - firstVisibleItemScrollOffset
- val target = if (abs(expectedDistance) < targetDistancePx) {
- expectedDistance
- } else {
- if (forward) targetDistancePx else -targetDistancePx
- }
-
- debugLog {
- "Scrolling to index=$index offset=$scrollOffset from " +
- "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
- "averageSize=$averageLineMainAxisSize and calculated target=$target"
- }
-
- anim = anim.copy(value = 0f)
- var prevValue = 0f
- anim.animateTo(
- target,
- sequentialAnimation = (anim.velocity != 0f)
- ) {
- // If we haven't found the item yet, check if it's visible.
- var targetItem = getTargetItem()
-
- if (targetItem == null) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue = if (target > 0) {
- value.coerceAtMost(target)
- } else {
- value.coerceAtLeast(target)
- }
- val delta = coercedValue - prevValue
- debugLog {
- "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
- }
-
- val consumed = scrollBy(delta)
- targetItem = getTargetItem()
- if (targetItem != null) {
- debugLog { "Found the item after performing scrollBy()" }
- } else if (!isOvershot()) {
- if (delta != consumed) {
- debugLog { "Hit end without finding the item" }
- cancelAnimation()
- loop = false
- return@animateTo
- }
- prevValue += delta
- if (forward) {
- if (value > boundDistancePx) {
- debugLog { "Struck bound going forward" }
- cancelAnimation()
- }
- } else {
- if (value < -boundDistancePx) {
- debugLog { "Struck bound going backward" }
- cancelAnimation()
- }
- }
-
- // Magic constants for teleportation chosen arbitrarily by experiment
- if (forward) {
- if (
- loops >= 2 &&
- index - layoutInfo.visibleItemsInfo.last().index > 200
- ) {
- // Teleport
- debugLog { "Teleport forward" }
- snapToItemIndexInternal(index = index - 200, scrollOffset = 0)
- }
- } else {
- if (
- loops >= 2 &&
- layoutInfo.visibleItemsInfo.first().index - index > 100
- ) {
- // Teleport
- debugLog { "Teleport backward" }
- snapToItemIndexInternal(index = index + 200, scrollOffset = 0)
- }
- }
- }
- }
-
- // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
- // the final position, there's no need to animate to it.
- if (isOvershot()) {
- debugLog { "Overshot" }
- snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
- loop = false
- cancelAnimation()
- return@animateTo
- } else if (targetItem != null) {
- debugLog { "Found item" }
- throw ItemFoundInScroll(
- targetItem,
- anim
- )
- }
- }
-
- loops++
- }
- } catch (itemFound: ItemFoundInScroll) {
- // We found it, animate to it
- // Bring to the requested position - will be automatically stopped if not possible
- val anim = itemFound.previousAnimation.copy(value = 0f)
- // TODO(b/191238807)
- val target = (itemFound.item.offset.y + scrollOffset).toFloat()
- var prevValue = 0f
- debugLog {
- "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
- }
- anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue = when {
- target > 0 -> {
- value.coerceAtMost(target)
- }
- target < 0 -> {
- value.coerceAtLeast(target)
- }
- else -> {
- debugLog { "WARNING: somehow ended up seeking 0px, this shouldn't happen" }
- 0f
- }
- }
- val delta = coercedValue - prevValue
- debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
- val consumed = scrollBy(delta)
- if (delta != consumed /* hit the end, stop */ ||
- coercedValue != value /* would have overshot, stop */
- ) {
- cancelAnimation()
- }
- prevValue += delta
- }
- // Once we're finished the animation, snap to the exact position to account for
- // rounding error (otherwise we tend to end up with the previous item scrolled the
- // tiniest bit onscreen)
- // TODO: prevent temporarily scrolling *past* the item
- snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun calculateLineAverageMainAxisSize(
- visibleItems: List<TvLazyGridItemInfo>,
- isVertical: Boolean
-): Int {
- val lineOf: (Int) -> Int = {
- if (isVertical) visibleItems[it].row else visibleItems[it].column
- }
-
- var totalLinesMainAxisSize = 0
- var linesCount = 0
-
- var lineStartIndex = 0
- while (lineStartIndex < visibleItems.size) {
- val currentLine = lineOf(lineStartIndex)
- if (currentLine == -1) {
- // Filter out exiting items.
- ++lineStartIndex
- continue
- }
-
- var lineMainAxisSize = 0
- var lineEndIndex = lineStartIndex
- while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
- lineMainAxisSize = max(
- lineMainAxisSize,
- if (isVertical) {
- visibleItems[lineEndIndex].size.height
- } else {
- visibleItems[lineEndIndex].size.width
- }
- )
- ++lineEndIndex
- }
-
- totalLinesMainAxisSize += lineMainAxisSize
- ++linesCount
-
- lineStartIndex = lineEndIndex
- }
-
- return totalLinesMainAxisSize / linesCount
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index e72ee1e..3a900332 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -22,7 +22,7 @@
@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+internal class LazyGridSpanLayoutProvider(private val itemProvider: LazyGridItemProvider) {
class LineConfiguration(val firstItemIndex: Int, val spans: List<TvGridItemSpan>)
/** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
@@ -61,7 +61,7 @@
List(currentSlotsPerLine) { TvGridItemSpan(1) }.also { previousDefaultSpans = it }
}
- val totalSize get() = itemsSnapshot.itemsCount
+ val totalSize get() = itemProvider.itemCount
/** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
var slotsPerLine = 0
@@ -73,7 +73,7 @@
}
fun getLineConfiguration(lineIndex: Int): LineConfiguration {
- if (!itemsSnapshot.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
// Quick return when all spans are 1x1 - in this case we can easily calculate positions.
val firstItemIndex = lineIndex * slotsPerLine
return LineConfiguration(
@@ -173,7 +173,7 @@
return LineIndex(0)
}
require(itemIndex < totalSize)
- if (!itemsSnapshot.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
return LineIndex(itemIndex / slotsPerLine)
}
@@ -211,7 +211,7 @@
return LineIndex(currentLine)
}
- private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+ private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
with(TvLazyGridItemSpanScopeImpl) {
maxCurrentLineSpan = maxSpan
maxLineSpan = slotsPerLine
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 5cf56f4..867f0c1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -25,8 +25,8 @@
@OptIn(ExperimentalFoundationApi::class)
internal class LazyMeasuredLineProvider(
private val isVertical: Boolean,
- slotSizesSums: List<Int>,
- crossAxisSpacing: Int,
+ private val slotSizesSums: List<Int>,
+ private val crossAxisSpacing: Int,
private val gridItemsCount: Int,
private val spaceBetweenLines: Int,
private val measuredItemProvider: LazyMeasuredItemProvider,
@@ -34,12 +34,12 @@
private val measuredLineFactory: MeasuredLineFactory
) {
// The constraints for cross axis size. The main axis is not restricted.
- internal val childConstraints: (startSlot: Int, span: Int) -> Constraints = { startSlot, span ->
+ internal fun childConstraints(startSlot: Int, span: Int): Constraints {
val lastSlotSum = slotSizesSums[startSlot + span - 1]
val prevSlotSum = if (startSlot == 0) 0 else slotSizesSums[startSlot - 1]
val slotsSize = lastSlotSum - prevSlotSum
- val crossAxisSize = slotsSize + crossAxisSpacing * (span - 1)
- if (isVertical) {
+ val crossAxisSize = (slotsSize + crossAxisSpacing * (span - 1)).coerceAtLeast(0)
+ return if (isVertical) {
Constraints.fixedWidth(crossAxisSize)
} else {
Constraints.fixedHeight(crossAxisSize)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
index ea19b91..27e5ca3 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -31,10 +32,57 @@
import androidx.compose.ui.semantics.scrollToIndex
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun rememberLazyGridSemanticState(
+ state: TvLazyGridState,
+ itemProvider: LazyLayoutItemProvider,
+ reverseScrolling: Boolean
+): LazyLayoutSemanticState =
+ remember(state, itemProvider, reverseScrolling) {
+ object : LazyLayoutSemanticState {
+ override fun scrollAxisRange(): ScrollAxisRange =
+ ScrollAxisRange(
+ value = {
+ // This is a simple way of representing the current position without
+ // needing any lazy items to be measured. It's good enough so far, because
+ // screen-readers care mostly about whether scroll position changed or not
+ // rather than the actual offset in pixels.
+ state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ },
+ maxValue = {
+ if (state.canScrollForward) {
+ // If we can scroll further, we don't know the end yet,
+ // but it's upper bounded by #items + 1
+ itemProvider.itemCount + 1f
+ } else {
+ // If we can't scroll further, the current value is the max
+ state.firstVisibleItemIndex +
+ state.firstVisibleItemScrollOffset / 100_000f
+ }
+ },
+ reverseScrolling = reverseScrolling
+ )
+
+ override suspend fun animateScrollBy(delta: Float) {
+ state.animateScrollBy(delta)
+ }
+
+ override suspend fun scrollToItem(index: Int) {
+ state.scrollToItem(index)
+ }
+
+ // TODO(popam): check if this is correct - it would be nice to provide correct columns
+ override fun collectionInfo(): CollectionInfo =
+ CollectionInfo(rowCount = -1, columnCount = -1)
+ }
+ }
+
+@OptIn(ExperimentalFoundationApi::class)
@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
@Composable
internal fun Modifier.lazyGridSemantics(
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
index bfd2510..2af17a5 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
@@ -17,6 +17,7 @@
package androidx.tv.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -66,9 +67,10 @@
}
}
+@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridIntervalContent(
- val key: ((index: Int) -> Any)?,
+ override val key: ((index: Int) -> Any)?,
val span: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan,
- val type: ((index: Int) -> Any?),
+ override val type: ((index: Int) -> Any?),
val item: @Composable TvLazyGridItemScope.(Int) -> Unit
-)
+) : LazyLayoutIntervalContent
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
index 15f05c7..52e13bf 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.lazy.layout.animateScrollToItem
import androidx.tv.foundation.lazy.list.AwaitFirstLayoutModifier
import kotlin.math.abs
@@ -172,7 +173,7 @@
/**
* The list of handles associated with the items from the [lineToPrefetch] line.
*/
- private var currentLinePrefetchHandles =
+ private val currentLinePrefetchHandles =
mutableVectorOf<LazyLayoutPrefetchState.PrefetchHandle>()
/**
@@ -210,6 +211,8 @@
internal var placementAnimator by mutableStateOf<LazyGridItemPlacementAnimator?>(null)
+ private val animateScrollScope = LazyGridAnimateScrollScope(this)
+
/**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
* pixels.
@@ -307,7 +310,6 @@
}
val info = layoutInfo
if (info.visibleItemsInfo.isNotEmpty()) {
- // check(isActive)
val scrollingForward = delta < 0
val lineToPrefetch: Int
val closestNextItemToPrefetch: Int
@@ -378,7 +380,7 @@
index: Int,
scrollOffset: Int = 0
) {
- doSmoothScrollToItem(index, scrollOffset, slotsPerLine)
+ animateScrollScope.animateScrollToItem(index, scrollOffset)
}
/**
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
similarity index 74%
rename from tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
rename to tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
index b78decf..06ecb8d 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
@@ -14,20 +14,20 @@
* limitations under the License.
*/
-package androidx.tv.foundation.lazy.list
+package androidx.tv.foundation.lazy.layout
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.copy
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastSumBy
+import java.lang.Math.abs
import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.abs
private class ItemFoundInScroll(
- val item: TvLazyListItemInfo,
+ val itemOffset: Int,
val previousAnimation: AnimationState<Float, AnimationVector1D>
) : CancellationException()
@@ -37,28 +37,52 @@
private const val DEBUG = false
private inline fun debugLog(generateMsg: () -> String) {
if (DEBUG) {
- println("LazyListScrolling: ${generateMsg()}")
+ println("LazyScrolling: ${generateMsg()}")
}
}
-internal suspend fun TvLazyListState.doSmoothScrollToItem(
+/**
+ * Abstraction over animated scroll for using [animateScrollToItem] in different layouts.
+ * todo(b/243786897): revisit this API and make it public
+ **/
+internal interface LazyAnimateScrollScope {
+ val density: Density
+
+ val firstVisibleItemIndex: Int
+
+ val firstVisibleItemScrollOffset: Int
+
+ val lastVisibleItemIndex: Int
+
+ val itemCount: Int
+
+ fun getTargetItemOffset(index: Int): Int?
+
+ fun ScrollScope.snapToItem(index: Int, scrollOffset: Int)
+
+ fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float
+
+ /** defines min number of items that forces scroll to snap if animation did not reach it */
+ val numOfItemsForTeleport: Int
+
+ suspend fun scroll(block: suspend ScrollScope.() -> Unit)
+}
+
+internal suspend fun LazyAnimateScrollScope.animateScrollToItem(
index: Int,
- scrollOffset: Int
+ scrollOffset: Int,
) {
- require(index >= 0f) { "Index should be non-negative ($index)" }
- fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
- it.index == index
- }
scroll {
+ require(index >= 0f) { "Index should be non-negative ($index)" }
try {
val targetDistancePx = with(density) { TargetDistance.toPx() }
val boundDistancePx = with(density) { BoundDistance.toPx() }
var loop = true
var anim = AnimationState(0f)
- val targetItemInitialInfo = getTargetItem()
- if (targetItemInitialInfo != null) {
+ val targetItemInitialOffset = getTargetItemOffset(index)
+ if (targetItemInitialOffset != null) {
// It's already visible, just animate directly
- throw ItemFoundInScroll(targetItemInitialInfo, anim)
+ throw ItemFoundInScroll(targetItemInitialOffset, anim)
}
val forward = index > firstVisibleItemIndex
@@ -91,12 +115,8 @@
}
var loops = 1
- while (loop && layoutInfo.totalItemsCount > 0) {
- val visibleItems = layoutInfo.visibleItemsInfo
- val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
- val indexesDiff = index - firstVisibleItemIndex
- val expectedDistance = (averageSize * indexesDiff).toFloat() +
- scrollOffset - firstVisibleItemScrollOffset
+ while (loop && itemCount > 0) {
+ val expectedDistance = expectedDistanceTo(index, scrollOffset)
val target = if (abs(expectedDistance) < targetDistancePx) {
expectedDistance
} else {
@@ -106,7 +126,7 @@
debugLog {
"Scrolling to index=$index offset=$scrollOffset from " +
"index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
- "averageSize=$averageSize and calculated target=$target"
+ " calculated target=$target"
}
anim = anim.copy(value = 0f)
@@ -116,9 +136,9 @@
sequentialAnimation = (anim.velocity != 0f)
) {
// If we haven't found the item yet, check if it's visible.
- var targetItem = getTargetItem()
+ var targetItemOffset = getTargetItemOffset(index)
- if (targetItem == null) {
+ if (targetItemOffset == null) {
// Springs can overshoot their target, clamp to the desired range
val coercedValue = if (target > 0) {
value.coerceAtMost(target)
@@ -131,8 +151,8 @@
}
val consumed = scrollBy(delta)
- targetItem = getTargetItem()
- if (targetItem != null) {
+ targetItemOffset = getTargetItemOffset(index)
+ if (targetItemOffset != null) {
debugLog { "Found the item after performing scrollBy()" }
} else if (!isOvershot()) {
if (delta != consumed) {
@@ -154,24 +174,29 @@
}
}
- // Magic constants for teleportation chosen arbitrarily by experiment
if (forward) {
if (
loops >= 2 &&
- index - layoutInfo.visibleItemsInfo.last().index > 100
+ index - lastVisibleItemIndex > numOfItemsForTeleport
) {
// Teleport
debugLog { "Teleport forward" }
- snapToItemIndexInternal(index = index - 100, scrollOffset = 0)
+ snapToItem(
+ index = index - numOfItemsForTeleport,
+ scrollOffset = 0
+ )
}
} else {
if (
loops >= 2 &&
- layoutInfo.visibleItemsInfo.first().index - index > 100
+ firstVisibleItemIndex - index > numOfItemsForTeleport
) {
// Teleport
debugLog { "Teleport backward" }
- snapToItemIndexInternal(index = index + 100, scrollOffset = 0)
+ snapToItem(
+ index = index + numOfItemsForTeleport,
+ scrollOffset = 0
+ )
}
}
}
@@ -181,13 +206,13 @@
// the final position, there's no need to animate to it.
if (isOvershot()) {
debugLog { "Overshot" }
- snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+ snapToItem(index = index, scrollOffset = scrollOffset)
loop = false
cancelAnimation()
return@animateTo
- } else if (targetItem != null) {
+ } else if (targetItemOffset != null) {
debugLog { "Found item" }
- throw ItemFoundInScroll(targetItem, anim)
+ throw ItemFoundInScroll(targetItemOffset, anim)
}
}
@@ -197,7 +222,7 @@
// We found it, animate to it
// Bring to the requested position - will be automatically stopped if not possible
val anim = itemFound.previousAnimation.copy(value = 0f)
- val target = (itemFound.item.offset + scrollOffset).toFloat()
+ val target = (itemFound.itemOffset + scrollOffset).toFloat()
var prevValue = 0f
debugLog {
"Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
@@ -230,7 +255,7 @@
// rounding error (otherwise we tend to end up with the previous item scrolled the
// tiniest bit onscreen)
// TODO: prevent temporarily scrolling *past* the item
- snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+ snapToItem(index = index, scrollOffset = scrollOffset)
}
}
}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
new file mode 100644
index 0000000..ddf0cfa
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 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.tv.foundation.lazy.layout
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.indexForKey
+import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Composable
+internal fun Modifier.lazyLayoutSemantics(
+ itemProvider: LazyLayoutItemProvider,
+ state: LazyLayoutSemanticState,
+ orientation: Orientation,
+ userScrollEnabled: Boolean
+): Modifier {
+ val coroutineScope = rememberCoroutineScope()
+ return this.then(
+ remember(
+ itemProvider,
+ state,
+ orientation,
+ userScrollEnabled
+ ) {
+ val isVertical = orientation == Orientation.Vertical
+ val indexForKeyMapping: (Any) -> Int = { needle ->
+ var result = -1
+ for (index in 0 until itemProvider.itemCount) {
+ if (itemProvider.getKey(index) == needle) {
+ result = index
+ break
+ }
+ }
+ result
+ }
+
+ val accessibilityScrollState = state.scrollAxisRange()
+
+ val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+ { x, y ->
+ val delta = if (isVertical) {
+ y
+ } else {
+ x
+ }
+ coroutineScope.launch {
+ state.animateScrollBy(delta)
+ }
+ // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+ true
+ }
+ } else {
+ null
+ }
+
+ val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+ { index ->
+ require(index >= 0 && index < itemProvider.itemCount) {
+ "Can't scroll to index $index, it is out of " +
+ "bounds [0, ${itemProvider.itemCount})"
+ }
+ coroutineScope.launch {
+ state.scrollToItem(index)
+ }
+ true
+ }
+ } else {
+ null
+ }
+
+ val collectionInfo = state.collectionInfo()
+
+ Modifier.semantics {
+ indexForKey(indexForKeyMapping)
+
+ if (isVertical) {
+ verticalScrollAxisRange = accessibilityScrollState
+ } else {
+ horizontalScrollAxisRange = accessibilityScrollState
+ }
+
+ if (scrollByAction != null) {
+ scrollBy(action = scrollByAction)
+ }
+
+ if (scrollToIndexAction != null) {
+ scrollToIndex(action = scrollToIndexAction)
+ }
+
+ this.collectionInfo = collectionInfo
+ }
+ }
+ )
+}
+
+internal interface LazyLayoutSemanticState {
+ fun scrollAxisRange(): ScrollAxisRange
+ fun collectionInfo(): CollectionInfo
+ suspend fun animateScrollBy(delta: Float)
+ suspend fun scrollToItem(index: Int)
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
index 64c697a..6510890 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
@@ -22,6 +22,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.PivotOffsets
/* Copied from
@@ -74,6 +75,30 @@
contentType: (index: Int) -> Any? = { null },
itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
)
+
+ /**
+ * Adds a sticky header item, which will remain pinned even when scrolling after it.
+ * The header will remain pinned until the next header will take its place.
+ *
+ * @sample androidx.compose.foundation.samples.StickyHeaderSample
+ *
+ * @param key a stable and unique key representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType the type of the content of this item. The item compositions of the same
+ * type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param content the content of the header
+ */
+ @ExperimentalTvFoundationApi
+ fun stickyHeader(
+ key: Any? = null,
+ contentType: Any? = null,
+ content: @Composable TvLazyListItemScope.() -> Unit
+ )
}
/**
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
index 69b09b8..cfb8ba4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.checkScrollableContainerConstraints
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
@@ -36,13 +37,13 @@
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.offset
import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.PivotOffsets
import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.tv.foundation.lazy.layout.lazyLayoutSemantics
import androidx.tv.foundation.lazy.lazyListBeyondBoundsModifier
import androidx.tv.foundation.lazy.lazyListPinningModifier
import androidx.tv.foundation.scrollableWithPivot
@@ -63,6 +64,8 @@
isVertical: Boolean,
/** Whether scrolling via the user gestures is allowed. */
userScrollEnabled: Boolean,
+ /** Number of items to layout before and after the visible items */
+ beyondBoundsItemCount: Int = 0,
/** offsets of child element within the parent and starting edge of the child from the pivot
* defined by the parentOffset. */
pivotOffsets: PivotOffsets,
@@ -77,7 +80,9 @@
/** The content of the list */
content: TvLazyListScope.() -> Unit
) {
- val itemProvider = rememberItemProvider(state, content)
+ val itemProvider = rememberLazyListItemProvider(state, content)
+ val semanticState =
+ rememberLazyListSemanticState(state, itemProvider, reverseLayout, isVertical)
val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
@@ -92,6 +97,7 @@
contentPadding,
reverseLayout,
isVertical,
+ beyondBoundsItemCount,
horizontalAlignment,
verticalAlignment,
horizontalArrangement,
@@ -107,12 +113,10 @@
modifier = modifier
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
- .lazyListSemantics(
+ .lazyLayoutSemantics(
itemProvider = itemProvider,
- state = state,
- coroutineScope = scope,
- isVertical = isVertical,
- reverseScrolling = reverseLayout,
+ state = semanticState,
+ orientation = orientation,
userScrollEnabled = userScrollEnabled
)
.clipScrollableContainer(orientation)
@@ -120,17 +124,11 @@
.lazyListPinningModifier(state, beyondBoundsInfo)
.scrollableWithPivot(
orientation = orientation,
- reverseDirection = run {
- // A finger moves with the content, not with the viewport. Therefore,
- // always reverse once to have "natural" gesture that goes reversed to layout
- var reverseDirection = !reverseLayout
- // But if rtl and horizontal, things move the other way around
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- if (isRtl && !isVertical) {
- reverseDirection = !reverseDirection
- }
- reverseDirection
- },
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseLayout
+ ),
state = state,
enabled = userScrollEnabled,
pivotOffsets = pivotOffsets
@@ -170,6 +168,8 @@
reverseLayout: Boolean,
/** The layout orientation of the list */
isVertical: Boolean,
+ /** Number of items to layout before and after the visible items */
+ beyondBoundsItemCount: Int,
/** The alignment to align items horizontally. Required when isVertical is true */
horizontalAlignment: Alignment.Horizontal? = null,
/** The alignment to align items vertically. Required when isVertical is false */
@@ -317,6 +317,7 @@
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
reverseLayout = reverseLayout,
+ beyondBoundsItemCount = beyondBoundsItemCount,
density = this,
placementAnimator = placementAnimator,
beyondBoundsInfo = beyondBoundsInfo,
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
new file mode 100644
index 0000000..510480d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastSumBy
+import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
+
+internal class LazyListAnimateScrollScope(
+ private val state: TvLazyListState
+) : LazyAnimateScrollScope {
+ override val density: Density get() = state.density
+
+ override val firstVisibleItemIndex: Int get() = state.firstVisibleItemIndex
+
+ override val firstVisibleItemScrollOffset: Int get() = state.firstVisibleItemScrollOffset
+
+ override val lastVisibleItemIndex: Int
+ get() = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+
+ override val itemCount: Int
+ get() = state.layoutInfo.totalItemsCount
+
+ override val numOfItemsForTeleport: Int = 100
+
+ override fun getTargetItemOffset(index: Int): Int? =
+ state.layoutInfo.visibleItemsInfo.fastFirstOrNull {
+ it.index == index
+ }?.offset
+
+ override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
+ state.snapToItemIndexInternal(index, scrollOffset)
+ }
+
+ override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
+ val visibleItems = state.layoutInfo.visibleItemsInfo
+ val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
+ val indexesDiff = index - firstVisibleItemIndex
+ return (averageSize * indexesDiff).toFloat() +
+ targetScrollOffset - firstVisibleItemScrollOffset
+ }
+
+ override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
+ state.scroll(block = block)
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
index 7d4137f..1cde8e1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
@@ -17,13 +17,79 @@
package androidx.tv.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
-internal sealed interface LazyListItemProvider : LazyLayoutItemProvider {
+internal interface LazyListItemProvider : LazyLayoutItemProvider {
/** The list of indexes of the sticky header items */
val headerIndexes: List<Int>
/** The scope used by the item content lambdas */
val itemScope: TvLazyListItemScopeImpl
}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyListItemProvider(
+ state: TvLazyListState,
+ content: TvLazyListScope.() -> Unit
+): LazyListItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = { state.firstVisibleItemIndex },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount }
+ )
+
+ return remember(nearestItemsRangeState) {
+ val itemScope = TvLazyListItemScopeImpl()
+ val itemProviderState = derivedStateOf {
+ val listScope = TvLazyListScopeImpl().apply(latestContent.value)
+ LazyListItemProviderImpl(
+ listScope.intervals,
+ nearestItemsRangeState.value,
+ listScope.headerIndexes,
+ itemScope
+ )
+ }
+ object : LazyListItemProvider,
+ LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+ override val headerIndexes: List<Int> get() = itemProviderState.value.headerIndexes
+ override val itemScope: TvLazyListItemScopeImpl get() =
+ itemProviderState.value.itemScope
+ }
+ }
+}
+
+@ExperimentalFoundationApi
+private class LazyListItemProviderImpl(
+ intervals: IntervalList<LazyListIntervalContent>,
+ nearestItemsRange: IntRange,
+ override val headerIndexes: List<Int>,
+ override val itemScope: TvLazyListItemScopeImpl
+) : LazyListItemProvider,
+ LazyLayoutItemProvider by LazyLayoutItemProvider(
+ intervals = intervals,
+ nearestItemsRange = nearestItemsRange,
+ itemContent = { interval: LazyListIntervalContent, index: Int ->
+ interval.item.invoke(itemScope, index)
+ }
+ )
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
deleted file mode 100644
index 1e9966d..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
- state: TvLazyListState,
- content: TvLazyListScope.() -> Unit
-): LazyListItemProvider {
- val latestContent = rememberUpdatedState(content)
- // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
- // of derivedState in return expr will only happen after the state value has been changed.
- val nearestItemsRangeState = remember(state) {
- mutableStateOf(
- Snapshot.withoutReadObservation {
- // State read is observed in composition, causing it to recompose 1 additional time.
- calculateNearestItemsRange(state.firstVisibleItemIndex)
- }
- )
- }
-
- LaunchedEffect(nearestItemsRangeState) {
- snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
- // MutableState's SnapshotMutationPolicy will make sure the provider is only
- // recreated when the state is updated with a new range.
- .collect { nearestItemsRangeState.value = it }
- }
- return remember(nearestItemsRangeState) {
- LazyListItemProviderImpl(
- derivedStateOf {
- val listScope = TvLazyListScopeImpl().apply(latestContent.value)
- LazyListItemsSnapshot(
- listScope.intervals,
- listScope.headerIndexes,
- nearestItemsRangeState.value
- )
- }
- )
- }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyListItemsSnapshot(
- private val intervals: IntervalList<LazyListIntervalContent>,
- val headerIndexes: List<Int>,
- nearestItemsRange: IntRange
-) {
- val itemsCount get() = intervals.size
-
- fun getKey(index: Int): Any {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- val key = interval.value.key?.invoke(localIntervalIndex)
- return key ?: getDefaultLazyLayoutKey(index)
- }
-
- @Composable
- fun Item(scope: TvLazyListItemScope, index: Int) {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- interval.value.item.invoke(scope, localIntervalIndex)
- }
-
- val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
- fun getContentType(index: Int): Any? {
- val interval = intervals[index]
- val localIntervalIndex = index - interval.startIndex
- return interval.value.type.invoke(localIntervalIndex)
- }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@ExperimentalFoundationApi
-internal class LazyListItemProviderImpl(
- private val itemsSnapshot: State<LazyListItemsSnapshot>
-) : LazyListItemProvider {
-
- override val itemScope = TvLazyListItemScopeImpl()
-
- override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
-
- override val itemCount get() = itemsSnapshot.value.itemsCount
-
- override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
- @Composable
- override fun Item(index: Int) {
- itemsSnapshot.value.Item(itemScope, index)
- }
-
- override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
- override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-}
-
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
- range: IntRange,
- list: IntervalList<LazyListIntervalContent>
-): Map<Any, Int> {
- val first = range.first
- check(first >= 0)
- val last = minOf(range.last, list.size - 1)
- return if (last < first) {
- emptyMap()
- } else {
- hashMapOf<Any, Int>().also { map ->
- list.forEach(
- fromIndex = first,
- toIndex = last,
- ) {
- if (it.value.key != null) {
- val keyFactory = requireNotNull(it.value.key)
- val start = maxOf(first, it.startIndex)
- val end = minOf(last, it.startIndex + it.size - 1)
- for (i in start..end) {
- map[keyFactory(i - it.startIndex)] = i
- }
- }
- }
- }
- }
-}
-
-/**
- * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
- val slidingWindowStart = VisibleItemsSlidingWindowSize *
- (firstVisibleItem / VisibleItemsSlidingWindowSize)
-
- val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
- val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
- return start until end
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private val VisibleItemsSlidingWindowSize = 30
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private val ExtraItemsNearTheSlidingWindow = 100
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
index 856d77b..dd1161b 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
@@ -57,6 +57,7 @@
density: Density,
placementAnimator: LazyListItemPlacementAnimator,
beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ beyondBoundsItemCount: Int,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyListMeasureResult {
require(beforeContentPadding >= 0)
@@ -227,31 +228,33 @@
}
}
- // Compose extra items before or after the visible items.
- fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
- fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
- val extraItemsBefore =
- if (beyondBoundsInfo.hasIntervals() &&
- visibleItems.first().index > beyondBoundsInfo.startIndex()) {
- mutableListOf<LazyMeasuredItem>().apply {
- for (i in visibleItems.first().index - 1 downTo beyondBoundsInfo.startIndex()) {
- add(itemProvider.getAndMeasure(DataIndex(i)))
- }
- }
- } else {
- emptyList()
- }
- val extraItemsAfter =
- if (beyondBoundsInfo.hasIntervals() &&
- visibleItems.last().index < beyondBoundsInfo.endIndex()) {
- mutableListOf<LazyMeasuredItem>().apply {
- for (i in visibleItems.last().index until beyondBoundsInfo.endIndex()) {
- add(itemProvider.getAndMeasure(DataIndex(i + 1)))
- }
- }
- } else {
- emptyList()
- }
+ // Compose extra items before
+ val extraItemsBefore = createItemsBeforeList(
+ beyondBoundsInfo = beyondBoundsInfo,
+ currentFirstItemIndex = currentFirstItemIndex,
+ itemProvider = itemProvider,
+ itemsCount = itemsCount,
+ beyondBoundsItemCount = beyondBoundsItemCount
+ )
+
+ // Update maxCrossAxis with extra items
+ extraItemsBefore.fastForEach {
+ maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+ }
+
+ // Compose items after last item
+ val extraItemsAfter = createItemsAfterList(
+ beyondBoundsInfo = beyondBoundsInfo,
+ visibleItems = visibleItems,
+ itemProvider = itemProvider,
+ itemsCount = itemsCount,
+ beyondBoundsItemCount = beyondBoundsItemCount
+ )
+
+ // Update maxCrossAxis with extra items
+ extraItemsAfter.fastForEach {
+ maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+ }
val noExtraItems = firstItem == visibleItems.first() &&
extraItemsBefore.isEmpty() &&
@@ -278,6 +281,15 @@
density = density,
)
+ placementAnimator.onMeasured(
+ consumedScroll = consumedScroll.toInt(),
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ reverseLayout = reverseLayout,
+ positionedItems = positionedItems,
+ itemProvider = itemProvider
+ )
+
val headerItem = if (headerIndexes.isNotEmpty()) {
findOrComposeLazyListHeader(
composedVisibleItems = positionedItems,
@@ -291,15 +303,6 @@
null
}
- placementAnimator.onMeasured(
- consumedScroll = consumedScroll.toInt(),
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- reverseLayout = reverseLayout,
- positionedItems = positionedItems,
- itemProvider = itemProvider
- )
-
return LazyListMeasureResult(
firstVisibleItem = firstItem,
firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
@@ -328,6 +331,109 @@
}
}
+private fun createItemsAfterList(
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ visibleItems: MutableList<LazyMeasuredItem>,
+ itemProvider: LazyMeasuredItemProvider,
+ itemsCount: Int,
+ beyondBoundsItemCount: Int
+): List<LazyMeasuredItem> {
+
+ fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
+
+ fun addItemsAfter(startIndex: Int, endIndex: Int): List<LazyMeasuredItem> {
+ return mutableListOf<LazyMeasuredItem>().apply {
+ for (i in startIndex until endIndex) {
+ val item = itemProvider.getAndMeasure(DataIndex(i + 1))
+ add(item)
+ }
+ }
+ }
+
+ val (startNonVisibleItems, endNonVisibleItems) = if (beyondBoundsItemCount != 0 &&
+ visibleItems.last().index + beyondBoundsItemCount <= itemsCount - 1
+ ) {
+ visibleItems.last().index to visibleItems.last().index + beyondBoundsItemCount
+ } else {
+ EmptyRange
+ }
+
+ val (startBeyondBoundItems, endBeyondBoundItems) = if (beyondBoundsInfo.hasIntervals() &&
+ visibleItems.last().index < beyondBoundsInfo.endIndex()
+ ) {
+ val start = (visibleItems.last().index + beyondBoundsItemCount).coerceAtMost(itemsCount - 1)
+ val end =
+ (beyondBoundsInfo.endIndex() + beyondBoundsItemCount).coerceAtMost(itemsCount - 1)
+ start to end
+ } else {
+ EmptyRange
+ }
+
+ return if (startNonVisibleItems.notInEmptyRange && startBeyondBoundItems.notInEmptyRange) {
+ addItemsAfter(
+ startNonVisibleItems,
+ endBeyondBoundItems
+ )
+ } else if (startNonVisibleItems.notInEmptyRange) {
+ addItemsAfter(startNonVisibleItems, endNonVisibleItems)
+ } else if (startBeyondBoundItems.notInEmptyRange) {
+ addItemsAfter(startBeyondBoundItems, endBeyondBoundItems)
+ } else {
+ emptyList()
+ }
+}
+
+private fun createItemsBeforeList(
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ currentFirstItemIndex: DataIndex,
+ itemProvider: LazyMeasuredItemProvider,
+ itemsCount: Int,
+ beyondBoundsItemCount: Int
+): List<LazyMeasuredItem> {
+
+ fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
+
+ fun addItemsBefore(startIndex: Int, endIndex: Int): List<LazyMeasuredItem> {
+ return mutableListOf<LazyMeasuredItem>().apply {
+ for (i in startIndex downTo endIndex) {
+ val item = itemProvider.getAndMeasure(DataIndex(i))
+ add(item)
+ }
+ }
+ }
+
+ val (startNonVisibleItems, endNonVisibleItems) =
+ if (beyondBoundsItemCount != 0 && currentFirstItemIndex.value - beyondBoundsItemCount > 0) {
+ currentFirstItemIndex.value - 1 to currentFirstItemIndex.value - beyondBoundsItemCount
+ } else {
+ EmptyRange
+ }
+
+ val (startBeyondBoundItems, endBeyondBoundItems) = if (beyondBoundsInfo.hasIntervals() &&
+ currentFirstItemIndex.value > beyondBoundsInfo.startIndex()
+ ) {
+ val start =
+ (currentFirstItemIndex.value - beyondBoundsItemCount - 1).coerceAtLeast(0)
+ val end = (beyondBoundsInfo.startIndex() - beyondBoundsItemCount).coerceAtLeast(0)
+ start to end
+ } else {
+ EmptyRange
+ }
+
+ return if (startNonVisibleItems.notInEmptyRange && startBeyondBoundItems.notInEmptyRange) {
+ addItemsBefore(
+ startNonVisibleItems,
+ endBeyondBoundItems
+ )
+ } else if (startNonVisibleItems.notInEmptyRange) {
+ addItemsBefore(startNonVisibleItems, endNonVisibleItems)
+ } else if (startBeyondBoundItems.notInEmptyRange) {
+ addItemsBefore(startBeyondBoundItems, endBeyondBoundItems)
+ } else {
+ emptyList()
+ }
+}
+
/**
* Calculates [LazyMeasuredItem]s offsets.
*/
@@ -428,3 +534,7 @@
}
return target
}
+
+private val EmptyRange = Int.MIN_VALUE to Int.MIN_VALUE
+private val Int.notInEmptyRange
+ get() = this != Int.MIN_VALUE
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
index 8242e4a..24230f3 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
@@ -17,6 +17,7 @@
package androidx.tv.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -90,7 +91,10 @@
@ExperimentalFoundationApi
fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
Snapshot.withoutReadObservation {
- update(findLazyListIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+ update(
+ DataIndex(itemProvider.findIndexByKey(lastKnownFirstItemKey, index.value)),
+ scrollOffset
+ )
}
}
@@ -103,34 +107,31 @@
this.scrollOffset = scrollOffset
}
}
+}
- private companion object {
- /**
- * Finds a position of the item with the given key in the lists. This logic allows us to
- * detect when there were items added or removed before our current first item.
- */
- @ExperimentalFoundationApi
- private fun findLazyListIndexByKey(
- key: Any?,
- lastKnownIndex: DataIndex,
- itemProvider: LazyListItemProvider
- ): DataIndex {
- if (key == null) {
- // there were no real item during the previous measure
- return lastKnownIndex
- }
- if (lastKnownIndex.value < itemProvider.itemCount &&
- key == itemProvider.getKey(lastKnownIndex.value)
- ) {
- // this item is still at the same index
- return lastKnownIndex
- }
- val newIndex = itemProvider.keyToIndexMap[key]
- if (newIndex != null) {
- return DataIndex(newIndex)
- }
- // fallback to the previous index if we don't know the new index of the item
- return lastKnownIndex
- }
+/**
+ * Finds a position of the item with the given key in the lists. This logic allows us to
+ * detect when there were items added or removed before our current first item.
+ */
+@ExperimentalFoundationApi
+internal fun LazyLayoutItemProvider.findIndexByKey(
+ key: Any?,
+ lastKnownIndex: Int,
+): Int {
+ if (key == null) {
+ // there were no real item during the previous measure
+ return lastKnownIndex
}
+ if (lastKnownIndex < itemCount &&
+ key == getKey(lastKnownIndex)
+ ) {
+ // this item is still at the same index
+ return lastKnownIndex
+ }
+ val newIndex = keyToIndexMap[key]
+ if (newIndex != null) {
+ return newIndex
+ }
+ // fallback to the previous index if we don't know the new index of the item
+ return lastKnownIndex
}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
index 2f0cc8c..2de3658 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
+import androidx.tv.foundation.lazy.layout.animateScrollToItem
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -88,6 +89,8 @@
private val scrollPosition =
LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
+ private val animateScrollScope = LazyListAnimateScrollScope(this)
+
/**
* The index of the first item that is visible.
*
@@ -309,7 +312,6 @@
}
val info = layoutInfo
if (info.visibleItemsInfo.isNotEmpty()) {
- // check(isActive)
val scrollingForward = delta < 0
val indexToPrefetch = if (scrollingForward) {
info.visibleItemsInfo.last().index + 1
@@ -335,6 +337,21 @@
}
}
+ private fun cancelPrefetchIfVisibleItemsChanged(info: TvLazyListLayoutInfo) {
+ if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
+ val expectedPrefetchIndex = if (wasScrollingForward) {
+ info.visibleItemsInfo.last().index + 1
+ } else {
+ info.visibleItemsInfo.first().index - 1
+ }
+ if (indexToPrefetch != expectedPrefetchIndex) {
+ indexToPrefetch = -1
+ currentPrefetchHandle?.cancel()
+ currentPrefetchHandle = null
+ }
+ }
+ }
+
internal val prefetchState = LazyLayoutPrefetchState()
/**
@@ -350,7 +367,7 @@
index: Int,
scrollOffset: Int = 0
) {
- doSmoothScrollToItem(index, scrollOffset)
+ animateScrollScope.animateScrollToItem(index, scrollOffset)
}
/**
@@ -370,21 +387,6 @@
cancelPrefetchIfVisibleItemsChanged(result)
}
- private fun cancelPrefetchIfVisibleItemsChanged(info: TvLazyListLayoutInfo) {
- if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
- val expectedPrefetchIndex = if (wasScrollingForward) {
- info.visibleItemsInfo.last().index + 1
- } else {
- info.visibleItemsInfo.first().index - 1
- }
- if (indexToPrefetch != expectedPrefetchIndex) {
- indexToPrefetch = -1
- currentPrefetchHandle?.cancel()
- currentPrefetchHandle = null
- }
- }
- }
-
/**
* When the user provided custom keys for the items we can try to detect when there were
* items added or removed before our current first visible item and keep this item
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
index 1e3bb60..560f0f1 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
@@ -17,129 +17,62 @@
package androidx.tv.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.ScrollAxisRange
-import androidx.compose.ui.semantics.collectionInfo
-import androidx.compose.ui.semantics.horizontalScrollAxisRange
-import androidx.compose.ui.semantics.indexForKey
-import androidx.compose.ui.semantics.scrollBy
-import androidx.compose.ui.semantics.scrollToIndex
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.verticalScrollAxisRange
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
+import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
// TODO (b/233188423): Address IllegalExperimentalApiUsage before moving to beta
@Suppress("ComposableModifierFactory", "ModifierInspectorInfo", "IllegalExperimentalApiUsage")
@ExperimentalFoundationApi
@Composable
-internal fun Modifier.lazyListSemantics(
- itemProvider: LazyListItemProvider,
+internal fun rememberLazyListSemanticState(
state: TvLazyListState,
- coroutineScope: CoroutineScope,
- isVertical: Boolean,
+ itemProvider: LazyLayoutItemProvider,
reverseScrolling: Boolean,
- userScrollEnabled: Boolean
-) = this.then(
- remember(
- itemProvider,
- state,
- isVertical,
- reverseScrolling,
- userScrollEnabled
- ) {
- val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemProvider::getKey
- var result = -1
- for (index in 0 until itemProvider.itemCount) {
- if (key(index) == needle) {
- result = index
- break
- }
- }
- result
- }
+ isVertical: Boolean
+): LazyLayoutSemanticState =
+ remember(state, itemProvider, reverseScrolling, isVertical) {
+ object : LazyLayoutSemanticState {
+ override fun scrollAxisRange(): ScrollAxisRange =
+ ScrollAxisRange(
+ value = {
+ // This is a simple way of representing the current position without
+ // needing any lazy items to be measured. It's good enough so far, because
+ // screen-readers care mostly about whether scroll position changed or not
+ // rather than the actual offset in pixels.
+ state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ },
+ maxValue = {
+ if (state.canScrollForward) {
+ // If we can scroll further, we don't know the end yet,
+ // but it's upper bounded by #items + 1
+ itemProvider.itemCount + 1f
+ } else {
+ // If we can't scroll further, the current value is the max
+ state.firstVisibleItemIndex +
+ state.firstVisibleItemScrollOffset / 100_000f
+ }
+ },
+ reverseScrolling = reverseScrolling
+ )
- val accessibilityScrollState = ScrollAxisRange(
- value = {
- // This is a simple way of representing the current position without
- // needing any lazy items to be measured. It's good enough so far, because
- // screen-readers care mostly about whether scroll position changed or not
- // rather than the actual offset in pixels.
- state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
- },
- maxValue = {
- if (state.canScrollForward) {
- // If we can scroll further, we don't know the end yet,
- // but it's upper bounded by #items + 1
- itemProvider.itemCount + 1f
+ override suspend fun animateScrollBy(delta: Float) {
+ state.animateScrollBy(delta)
+ }
+
+ override suspend fun scrollToItem(index: Int) {
+ state.scrollToItem(index)
+ }
+
+ override fun collectionInfo(): CollectionInfo =
+ if (isVertical) {
+ CollectionInfo(rowCount = -1, columnCount = 1)
} else {
- // If we can't scroll further, the current value is the max
- state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ CollectionInfo(rowCount = 1, columnCount = -1)
}
- },
- reverseScrolling = reverseScrolling
- )
- val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
- { x, y ->
- val delta = if (isVertical) {
- y
- } else {
- x
- }
- coroutineScope.launch {
- (state as ScrollableState).animateScrollBy(delta)
- }
- // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
- true
- }
- } else {
- null
}
-
- val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
- { index ->
- require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
- "Can't scroll to index $index, it is out of " +
- "bounds [0, ${state.layoutInfo.totalItemsCount})"
- }
- coroutineScope.launch {
- state.scrollToItem(index)
- }
- true
- }
- } else {
- null
- }
-
- val collectionInfo = CollectionInfo(
- rowCount = if (isVertical) -1 else 1,
- columnCount = if (isVertical) 1 else -1
- )
-
- Modifier.semantics {
- indexForKey(indexForKeyMapping)
-
- if (isVertical) {
- verticalScrollAxisRange = accessibilityScrollState
- } else {
- horizontalScrollAxisRange = accessibilityScrollState
- }
-
- if (scrollByAction != null) {
- scrollBy(action = scrollByAction)
- }
-
- if (scrollToIndexAction != null) {
- scrollToIndex(action = scrollToIndexAction)
- }
-
- this.collectionInfo = collectionInfo
- }
- }
-)
+ }
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
index cc31459..89559d8 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
@@ -57,27 +57,32 @@
* The size of the viewport in pixels. It is the lazy list layout size including all the
* content paddings.
*/
+ // DO NOT ADD DEFAULT get() HERE
val viewportSize: IntSize
/**
* The orientation of the lazy list.
*/
+ // DO NOT ADD DEFAULT get() HERE
val orientation: Orientation
/**
* True if the direction of scrolling and layout is reversed.
*/
+ // DO NOT ADD DEFAULT get() HERE
val reverseLayout: Boolean
/**
* The content padding in pixels applied before the first item in the direction of scrolling.
* For example it is a top content padding for LazyColumn with reverseLayout set to false.
*/
+ // DO NOT ADD DEFAULT get() HERE
val beforeContentPadding: Int
/**
* The content padding in pixels applied after the last item in the direction of scrolling.
* For example it is a bottom content padding for LazyColumn with reverseLayout set to false.
*/
+ // DO NOT ADD DEFAULT get() HERE
val afterContentPadding: Int
}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
index e91c249..23e84e4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
@@ -18,8 +18,10 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
+import androidx.tv.foundation.ExperimentalTvFoundationApi
@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@OptIn(ExperimentalFoundationApi::class)
@@ -61,10 +63,25 @@
)
)
}
+
+ @ExperimentalTvFoundationApi
+ override fun stickyHeader(
+ key: Any?,
+ contentType: Any?,
+ content: @Composable TvLazyListItemScope.() -> Unit
+ ) {
+ val headersIndexes = _headerIndexes ?: mutableListOf<Int>().also {
+ _headerIndexes = it
+ }
+ headersIndexes.add(_intervals.size)
+
+ item(key, contentType, content)
+ }
}
+@OptIn(ExperimentalFoundationApi::class)
internal class LazyListIntervalContent(
- val key: ((index: Int) -> Any)?,
- val type: ((index: Int) -> Any?),
+ override val key: ((index: Int) -> Any)?,
+ override val type: ((index: Int) -> Any?),
val item: @Composable TvLazyListItemScope.(index: Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 50fc402..892bde3 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -68,11 +68,3 @@
description = "build TV applications using controls that adhere to Material Design Language."
targetsJavaConsumers = false
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
index 9cda913..6ed68d5 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
@@ -34,6 +34,7 @@
import androidx.tv.material.TabDefaults
import androidx.tv.material.TabRow
import androidx.tv.material.TabRowDefaults
+import kotlinx.coroutines.delay
enum class Navigation(val displayName: String, val action: @Composable () -> Unit) {
LazyRowsAndColumns("Lazy Rows and Columns", { LazyRowsAndColumns() }),
@@ -55,7 +56,12 @@
updateSelectedTab = { selectedTabIndex = it }
)
- LaunchedEffect(selectedTabIndex) { updateSelectedTab(Navigation.values()[selectedTabIndex]) }
+ LaunchedEffect(selectedTabIndex) {
+ // Only update the tab after 250 milliseconds to avoid loading intermediate tabs while
+ // fast scrolling in the TabRow
+ delay(250)
+ updateSelectedTab(Navigation.values()[selectedTabIndex])
+ }
}
/**
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 4d7713c..c4d2004 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -106,7 +106,7 @@
method @androidx.compose.runtime.Composable public static void CurvedLayout(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional androidx.wear.compose.foundation.CurvedAlignment.Radial? radialAlignment, optional int angularDirection, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> contentBuilder);
}
- @androidx.compose.runtime.Stable public sealed interface CurvedModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public sealed interface CurvedModifier {
method public default infix androidx.wear.compose.foundation.CurvedModifier then(androidx.wear.compose.foundation.CurvedModifier other);
field public static final androidx.wear.compose.foundation.CurvedModifier.Companion Companion;
}
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index 4d7713c..c4d2004 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -106,7 +106,7 @@
method @androidx.compose.runtime.Composable public static void CurvedLayout(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional androidx.wear.compose.foundation.CurvedAlignment.Radial? radialAlignment, optional int angularDirection, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> contentBuilder);
}
- @androidx.compose.runtime.Stable public sealed interface CurvedModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public sealed interface CurvedModifier {
method public default infix androidx.wear.compose.foundation.CurvedModifier then(androidx.wear.compose.foundation.CurvedModifier other);
field public static final androidx.wear.compose.foundation.CurvedModifier.Companion Companion;
}
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 4d7713c..c4d2004 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -106,7 +106,7 @@
method @androidx.compose.runtime.Composable public static void CurvedLayout(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional androidx.wear.compose.foundation.CurvedAlignment.Radial? radialAlignment, optional int angularDirection, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> contentBuilder);
}
- @androidx.compose.runtime.Stable public sealed interface CurvedModifier {
+ @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public sealed interface CurvedModifier {
method public default infix androidx.wear.compose.foundation.CurvedModifier then(androidx.wear.compose.foundation.CurvedModifier other);
field public static final androidx.wear.compose.foundation.CurvedModifier.Companion Companion;
}
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 716a32a..cca24b1 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -102,6 +102,10 @@
testOptions.unitTests.includeAndroidResources = true
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/wear/compose/foundation"
+
+ buildTypes.all {
+ consumerProguardFiles("proguard-rules.pro")
+ }
namespace "androidx.wear.compose.foundation"
}
diff --git a/wear/compose/compose-foundation/proguard-rules.pro b/wear/compose/compose-foundation/proguard-rules.pro
new file mode 100644
index 0000000..9c18e80
--- /dev/null
+++ b/wear/compose/compose-foundation/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+-keepnames class androidx.wear.compose.foundation.BasicCurvedTextKt
+
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
index c48a7389..fd7d85d 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
+@JvmDefaultWithCompatibility
/**
* An ordered, immutable, collection of modifier elements that work with curved components, in a
* polar coordinate space.
diff --git a/wear/compose/compose-material-core/api/current.txt b/wear/compose/compose-material-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material-core/api/public_plus_experimental_current.txt b/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material-core/api/res-current.txt b/wear/compose/compose-material-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/compose/compose-material-core/api/res-current.txt
diff --git a/wear/compose/compose-material-core/api/restricted_current.txt b/wear/compose/compose-material-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material-core/build.gradle b/wear/compose/compose-material-core/build.gradle
new file mode 100644
index 0000000..30aac6a
--- /dev/null
+++ b/wear/compose/compose-material-core/build.gradle
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.AndroidXComposePlugin
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+}
+
+// Disable multi-platform.
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+ if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ api(project(":compose:foundation:foundation"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-text"))
+ api(project(":compose:runtime:runtime"))
+
+ implementation(libs.kotlinStdlib)
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:material:material-icons-core"))
+ implementation(project(":compose:material:material-ripple"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":wear:compose:compose-foundation"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+
+ androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.truth)
+
+ testImplementation(libs.testRules)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+ }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ androidXComposeMultiplatform {
+ android()
+ desktop()
+ }
+
+ kotlin {
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlinStdlibCommon)
+
+ api(project(":compose:foundation:foundation"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-text"))
+ api(project(":compose:runtime:runtime"))
+ api("androidx.annotation:annotation:1.1.0")
+
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:material:material-icons-core"))
+ implementation(project(":compose:material:material-ripple"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":wear:compose:compose-foundation"))
+ }
+ jvmMain.dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+
+ commonTest.dependencies {
+ implementation(kotlin("test-junit"))
+ }
+ androidAndroidTest.dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.truth)
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":compose:ui:ui-test"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
+ implementation(project(":test:screenshot:screenshot"))
+ }
+ }
+ }
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 25
+ }
+ // Use Robolectric 4.+
+ testOptions.unitTests.includeAndroidResources = true
+ sourceSets.androidTest.assets.srcDirs +=
+ project.rootDir.absolutePath + "/../../golden/wear/compose/materialcore"
+ buildTypes.all {
+ consumerProguardFiles("proguard-rules.pro")
+ }
+ namespace "androidx.wear.compose.material.core"
+ lint {
+ baseline = file("lint-baseline.xml")
+ }
+}
+
+androidx {
+ name = "Android Wear Compose Material Core"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.WEAR_COMPOSE
+ inceptionYear = "2022"
+ description = "WearOS Compose Material Core Library. This library contains themeless " +
+ "components that are shared between different WearOS Compose Material libraries. It " +
+ "builds upon the Jetpack Compose libraries."
+ targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=all",
+ ]
+ }
+}
diff --git a/wear/compose/compose-material-core/proguard-rules.pro b/wear/compose/compose-material-core/proguard-rules.pro
new file mode 100644
index 0000000..5987eb2
--- /dev/null
+++ b/wear/compose/compose-material-core/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+# -keepnames class androidx.wear.compose.material.MaterialTheme
diff --git a/wear/compose/compose-material-core/src/androidMain/AndroidManifest.xml b/wear/compose/compose-material-core/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..e0788d6
--- /dev/null
+++ b/wear/compose/compose-material-core/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest />
diff --git a/wear/compose/compose-material/api/current.ignore b/wear/compose/compose-material/api/current.ignore
new file mode 100644
index 0000000..05e58f9
--- /dev/null
+++ b/wear/compose/compose-material/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
+ Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index c87b5b0..26422ef 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -634,7 +634,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
- field public static final String TimeFormat12Hours = "h:mm a";
+ field public static final String TimeFormat12Hours = "h:mm";
field public static final String TimeFormat24Hours = "HH:mm";
}
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index da6559b..a231a61 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -735,7 +735,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
- field public static final String TimeFormat12Hours = "h:mm a";
+ field public static final String TimeFormat12Hours = "h:mm";
field public static final String TimeFormat24Hours = "HH:mm";
}
diff --git a/wear/compose/compose-material/api/restricted_current.ignore b/wear/compose/compose-material/api/restricted_current.ignore
new file mode 100644
index 0000000..05e58f9
--- /dev/null
+++ b/wear/compose/compose-material/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
+ Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index c87b5b0..26422ef 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -634,7 +634,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
- field public static final String TimeFormat12Hours = "h:mm a";
+ field public static final String TimeFormat12Hours = "h:mm";
field public static final String TimeFormat24Hours = "HH:mm";
}
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index d19e7c5..52f456a 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -36,10 +36,11 @@
implementation(libs.kotlinStdlib)
implementation(project(":compose:animation:animation"))
- implementation(project(":compose:material:material"))
+ implementation(project(":compose:material:material-icons-core"))
implementation(project(":compose:material:material-ripple"))
implementation(project(":compose:ui:ui-util"))
implementation(project(":wear:compose:compose-foundation"))
+ implementation(project(":wear:compose:compose-material-core"))
implementation("androidx.profileinstaller:profileinstaller:1.2.0")
androidTestImplementation(project(":compose:ui:ui-test"))
@@ -80,10 +81,11 @@
api("androidx.annotation:annotation:1.1.0")
implementation(project(":compose:animation:animation"))
- implementation(project(":compose:material:material"))
+ implementation(project(":compose:material:material-icons-core"))
implementation(project(":compose:material:material-ripple"))
implementation(project(":compose:ui:ui-util"))
implementation(project(":wear:compose:compose-foundation"))
+ implementation(project(":wear:compose:compose-material-core"))
}
jvmMain.dependencies {
implementation(libs.kotlinStdlib)
@@ -118,6 +120,9 @@
testOptions.unitTests.includeAndroidResources = true
sourceSets.androidTest.assets.srcDirs +=
project.rootDir.absolutePath + "/../../golden/wear/compose/material"
+ buildTypes.all {
+ consumerProguardFiles("proguard-rules.pro")
+ }
namespace "androidx.wear.compose.material"
lint {
baseline = file("lint-baseline.xml")
@@ -135,11 +140,3 @@
" libraries."
targetsJavaConsumers = false
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=all",
- ]
- }
-}
diff --git a/wear/compose/compose-material/proguard-rules.pro b/wear/compose/compose-material/proguard-rules.pro
new file mode 100644
index 0000000..a38fa51
--- /dev/null
+++ b/wear/compose/compose-material/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+-keepnames class androidx.wear.compose.material.MaterialTheme
+-keepnames class androidx.wear.compose.material.ScalingLazyColumnKt
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
index d83eac1..a71d872 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/MaterialTest.kt
@@ -19,9 +19,9 @@
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.runtime.Composable
@@ -67,11 +67,11 @@
fun ComposeContentTestRule.setContentWithTheme(
modifier: Modifier = Modifier,
- composable: @Composable () -> Unit
+ composable: @Composable BoxScope.() -> Unit
) {
setContent {
MaterialTheme {
- Surface(modifier = modifier, content = composable)
+ Box(modifier = modifier, content = composable)
}
}
}
@@ -84,16 +84,14 @@
): SemanticsNodeInteraction {
setContent {
MaterialTheme {
- Surface {
- Box {
- Box(
- Modifier.sizeIn(
- maxWidth = parentMaxWidth,
- maxHeight = parentMaxHeight
- ).testTag("containerForSizeAssertion")
- ) {
- content()
- }
+ Box {
+ Box(
+ Modifier.sizeIn(
+ maxWidth = parentMaxWidth,
+ maxHeight = parentMaxHeight
+ ).testTag("containerForSizeAssertion")
+ ) {
+ content()
}
}
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
index a20b5bc..936958e 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
@@ -123,9 +123,9 @@
)
}
rule.waitForIdle()
- // by default fully filled progress approximately takes 23-26% of the control
+ // by default fully filled progress approximately takes 23-27% of the control
rule.onNodeWithTag(TEST_TAG).captureToImage()
- .assertColorInPercentageRange(Color.Yellow, 23f..26f)
+ .assertColorInPercentageRange(Color.Yellow, 23f..27f)
rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Red)
}
@@ -142,9 +142,9 @@
}
rule.waitForIdle()
rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Yellow)
- // by default progress track approximately takes 23-26% of the control
+ // by default progress track approximately takes 23-27% of the control
rule.onNodeWithTag(TEST_TAG).captureToImage()
- .assertColorInPercentageRange(Color.Red, 23f..26f)
+ .assertColorInPercentageRange(Color.Red, 23f..27f)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -241,7 +241,7 @@
rule.waitForIdle()
// Because of the stroke cap, progress color takes a little bit more space than track color
rule.onNodeWithTag(TEST_TAG).captureToImage()
- .assertColorInPercentageRange(Color.Yellow, 24f..27f)
+ .assertColorInPercentageRange(Color.Yellow, 24f..28f)
rule.onNodeWithTag(TEST_TAG).captureToImage()
.assertColorInPercentageRange(Color.Red, 18f..23f)
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
index 382300c..e9644a6 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.unit.sp
import androidx.wear.compose.foundation.curvedComposable
import java.util.Calendar
+import java.util.TimeZone
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -541,6 +542,22 @@
}
assertEquals(convertedTime, actualTime)
}
+
+ @Test
+ fun formats_current_time_12H() {
+ val currentTimeInMillis = 1631544258000L // 2021-09-13 14:44:18
+ val expectedTime = "2:44"
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
+
+ var actualTime: String? = null
+ rule.setContentWithTheme {
+ actualTime = currentTime(
+ { currentTimeInMillis },
+ TimeTextDefaults.TimeFormat12Hours
+ ).value
+ }
+ assertEquals(expectedTime, actualTime)
+ }
}
private const val LINEAR_ITEM_TAG = "LINEAR_ITEM_TAG"
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
index 494693d..9e694ec 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
@@ -18,6 +18,7 @@
import android.os.Build
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
@@ -124,7 +125,7 @@
private fun verifyScreenshot(
threshold: Double = 0.98,
- content: @Composable () -> Unit
+ content: @Composable BoxScope.() -> Unit
) {
rule.setContentWithTheme(composable = content)
rule.onNodeWithTag(TEST_TAG)
diff --git a/wear/compose/compose-material/src/androidMain/baseline-prof.txt b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
index 2eb04f1..f2a2297 100644
--- a/wear/compose/compose-material/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
@@ -1,5 +1,5 @@
HPLandroidx/wear/compose/material/AbstractPlaceholderModifier;->**(**)**
-PLandroidx/wear/compose/material/AnimationKt**->**(**)**
+SPLandroidx/wear/compose/material/AnimationKt**->**(**)**
PLandroidx/wear/compose/material/AutoCenteringParams;->**(**)**
Landroidx/wear/compose/material/ButtonBorder;
Landroidx/wear/compose/material/ButtonColors;
@@ -8,7 +8,7 @@
HSPLandroidx/wear/compose/material/CardDefaults;->**(**)**
HSPLandroidx/wear/compose/material/CardKt**->**(**)**
Landroidx/wear/compose/material/CheckboxColors;
-HPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
Landroidx/wear/compose/material/ChipBorder;
Landroidx/wear/compose/material/ChipColors;
HSPLandroidx/wear/compose/material/ChipDefaults;->**(**)**
@@ -22,16 +22,16 @@
HSPLandroidx/wear/compose/material/CurvedTextKt**->**(**)**
HSPLandroidx/wear/compose/material/DefaultButtonBorder;->**(**)**
HSPLandroidx/wear/compose/material/DefaultButtonColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
HSPLandroidx/wear/compose/material/DefaultChipColors;->**(**)**
HSPLandroidx/wear/compose/material/DefaultInlineSliderColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
HSPLandroidx/wear/compose/material/DefaultScalingLazyListItemInfo;->**(**)**
HSPLandroidx/wear/compose/material/DefaultScalingLazyListLayoutInfo;->**(**)**
HSPLandroidx/wear/compose/material/DefaultScalingParams;->**(**)**
HSPLandroidx/wear/compose/material/DefaultSplitToggleChipColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
SPLandroidx/wear/compose/material/DefaultTimeSource;->**(**)**
HSPLandroidx/wear/compose/material/DefaultTimeSourceKt**->**(**)**
SPLandroidx/wear/compose/material/DefaultToggleButtonColors;->**(**)**
@@ -53,17 +53,17 @@
HSPLandroidx/wear/compose/material/MaterialTheme;->**(**)**
SPLandroidx/wear/compose/material/MaterialThemeKt**->**(**)**
HSPLandroidx/wear/compose/material/Modifiers;->**(**)**
-PLandroidx/wear/compose/material/PainterWithBrushOverlay;->**(**)**
HSPLandroidx/wear/compose/material/PickerDefaults;->**(**)**
HSPLandroidx/wear/compose/material/PickerKt**->**(**)**
SPLandroidx/wear/compose/material/PickerScopeImpl;->**(**)**
HSPLandroidx/wear/compose/material/PickerState;->**(**)**
+PLandroidx/wear/compose/material/PlaceholderBackgroundPainter;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderDefaults;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderState;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
Landroidx/wear/compose/material/PositionIndicatorState;
@@ -71,8 +71,9 @@
PLandroidx/wear/compose/material/ProgressIndicatorDefaults;->**(**)**
HPLandroidx/wear/compose/material/ProgressIndicatorKt**->**(**)**
Landroidx/wear/compose/material/R;
-HPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
-HSPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
+Landroidx/wear/compose/material/RadioButtonColors;
+HSPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
+SPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
SPLandroidx/wear/compose/material/RangeDefaultsKt**->**(**)**
SPLandroidx/wear/compose/material/RangeIcons;->**(**)**
HSPLandroidx/wear/compose/material/ResistanceConfig;->**(**)**
@@ -100,7 +101,6 @@
HSPLandroidx/wear/compose/material/ShapesKt**->**(**)**
HSPLandroidx/wear/compose/material/SliderKt**->**(**)**
Landroidx/wear/compose/material/SplitToggleChipColors;
-SPLandroidx/wear/compose/material/SqueezeMotion;->**(**)**
SPLandroidx/wear/compose/material/StepperDefaults;->**(**)**
HSPLandroidx/wear/compose/material/StepperKt**->**(**)**
Landroidx/wear/compose/material/SwipeProgress;
@@ -113,7 +113,7 @@
HSPLandroidx/wear/compose/material/SwipeableKt**->**(**)**
HSPLandroidx/wear/compose/material/SwipeableState;->**(**)**
Landroidx/wear/compose/material/SwitchColors;
-HPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
HSPLandroidx/wear/compose/material/TextKt**->**(**)**
Landroidx/wear/compose/material/ThresholdConfig;
SPLandroidx/wear/compose/material/TimeBroadcastReceiver;->**(**)**
@@ -126,8 +126,8 @@
Landroidx/wear/compose/material/ToggleChipColors;
HSPLandroidx/wear/compose/material/ToggleChipDefaults;->**(**)**
HSPLandroidx/wear/compose/material/ToggleChipKt**->**(**)**
-HPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
-PLandroidx/wear/compose/material/ToggleStage;->**(**)**
+HSPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
+SPLandroidx/wear/compose/material/ToggleStage;->**(**)**
HSPLandroidx/wear/compose/material/Typography;->**(**)**
HSPLandroidx/wear/compose/material/TypographyKt**->**(**)**
HSPLandroidx/wear/compose/material/VignetteKt**->**(**)**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
index 2ff5ef9..cb8a7d4 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
@@ -157,7 +157,7 @@
/**
* Default format for 12h clock.
*/
- const val TimeFormat12Hours = "h:mm a"
+ const val TimeFormat12Hours = "h:mm"
/**
* The default content padding used by [TimeText]
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material3/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material3/api/public_plus_experimental_current.txt b/wear/compose/compose-material3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material3/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material3/api/res-current.txt b/wear/compose/compose-material3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/compose/compose-material3/api/res-current.txt
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/compose/compose-material3/benchmark/benchmark-proguard-rules.pro b/wear/compose/compose-material3/benchmark/benchmark-proguard-rules.pro
new file mode 100644
index 0000000..e4061d2
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/benchmark-proguard-rules.pro
@@ -0,0 +1,37 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-dontobfuscate
+
+-ignorewarnings
+
+-keepattributes *Annotation*
+
+-dontnote junit.framework.**
+-dontnote junit.runner.**
+
+-dontwarn androidx.test.**
+-dontwarn org.junit.**
+-dontwarn org.hamcrest.**
+-dontwarn com.squareup.javawriter.JavaWriter
+
+-keepclasseswithmembers @org.junit.runner.RunWith public class *
\ No newline at end of file
diff --git a/wear/compose/compose-material3/benchmark/build.gradle b/wear/compose/compose-material3/benchmark/build.gradle
new file mode 100644
index 0000000..9bd32e84
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 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.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+ id("androidx.benchmark")
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 25
+ }
+ buildTypes.all {
+ consumerProguardFiles "benchmark-proguard-rules.pro"
+ }
+ namespace "androidx.wear.compose.material3.benchmark"
+}
+
+dependencies {
+
+ androidTestImplementation project(":benchmark:benchmark-junit4")
+ androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
+ androidTestImplementation project(":compose:foundation:foundation")
+ androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation project(":wear:compose:compose-foundation")
+ androidTestImplementation project(":wear:compose:compose-material3")
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.kotlinReflect)
+ androidTestImplementation(libs.kotlinTestCommon)
+ androidTestImplementation(libs.truth)
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
new file mode 100644
index 0000000..297ea36
--- /dev/null
+++ b/wear/compose/compose-material3/build.gradle
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.AndroidXComposePlugin
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+}
+
+// Disable multi-platform.
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+ if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ api(project(":compose:foundation:foundation"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-text"))
+ api(project(":compose:runtime:runtime"))
+
+ implementation(libs.kotlinStdlib)
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:material:material-icons-core"))
+ implementation(project(":compose:material:material-ripple"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":wear:compose:compose-foundation"))
+ implementation(project(":wear:compose:compose-material-core"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+
+ androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.truth)
+
+ testImplementation(libs.testRules)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+
+ samples(project(":wear:compose:compose-material3-samples"))
+ }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ androidXComposeMultiplatform {
+ android()
+ desktop()
+ }
+
+ kotlin {
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlinStdlibCommon)
+
+ api(project(":compose:foundation:foundation"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-text"))
+ api(project(":compose:runtime:runtime"))
+ api("androidx.annotation:annotation:1.1.0")
+
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:material:material-icons-core"))
+ implementation(project(":compose:material:material-ripple"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":wear:compose:compose-foundation"))
+ implementation(project(":wear:compose:compose-material-core"))
+ }
+ jvmMain.dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+
+ commonTest.dependencies {
+ implementation(kotlin("test-junit"))
+ }
+ androidAndroidTest.dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.truth)
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":compose:ui:ui-test"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
+ implementation(project(":test:screenshot:screenshot"))
+ }
+ }
+ }
+ dependencies {
+ samples(project(":wear:compose:compose-material3-samples"))
+ }
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 25
+ }
+ // Use Robolectric 4.+
+ testOptions.unitTests.includeAndroidResources = true
+ sourceSets.androidTest.assets.srcDirs +=
+ project.rootDir.absolutePath + "/../../golden/wear/compose/material3"
+ buildTypes.all {
+ consumerProguardFiles("proguard-rules.pro")
+ }
+ namespace "androidx.wear.compose.material3"
+ lint {
+ baseline = file("lint-baseline.xml")
+ }
+}
+
+androidx {
+ name = "Android Wear Compose Material 3"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.WEAR_COMPOSE_MATERIAL3
+ mavenGroup = LibraryGroups.WEAR_COMPOSE
+ inceptionYear = "2022"
+ description = "WearOS Compose Material 3 Library. This library makes it easier for " +
+ "developers to write Jetpack Compose applications for Wearable devices that " +
+ "implement Wear Material 3 Design UX guidelines and specifications. It builds upon " +
+ "the Jetpack Compose libraries."
+ targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=all",
+ ]
+ }
+}
diff --git a/wear/compose/compose-material3/proguard-rules.pro b/wear/compose/compose-material3/proguard-rules.pro
new file mode 100644
index 0000000..ae637fe
--- /dev/null
+++ b/wear/compose/compose-material3/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+# -keepnames class androidx.wear.compose.material3.MaterialTheme
diff --git a/wear/compose/compose-material3/samples/build.gradle b/wear/compose/compose-material3/samples/build.gradle
new file mode 100644
index 0000000..03b746a
--- /dev/null
+++ b/wear/compose/compose-material3/samples/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+
+ implementation(libs.kotlinStdlib)
+
+ compileOnly(project(":annotation:annotation-sampled"))
+ implementation(project(":compose:animation:animation-graphics"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":compose:material:material-icons-core"))
+ implementation(project(":wear:compose:compose-material3"))
+ implementation(project(":wear:compose:compose-foundation"))
+
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.truth)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 25
+ }
+ namespace "androidx.wear.compose.material3.samples"
+}
+
+androidx {
+ name = "Android Wear Compose Material 3 Samples"
+ type = LibraryType.SAMPLES
+ mavenGroup = LibraryGroups.WEAR_COMPOSE
+ inceptionYear = "2022"
+ description = "Contains the sample code for the Android Wear Compose Material 3 Classes"
+}
diff --git a/wear/compose/compose-material3/src/androidMain/AndroidManifest.xml b/wear/compose/compose-material3/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..e0788d6
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest />
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 2f3d8cc..4f3ab02 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -51,6 +51,9 @@
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
+ buildTypes.all {
+ consumerProguardFiles("proguard-rules.pro")
+ }
namespace "androidx.wear.compose.navigation"
}
diff --git a/wear/compose/compose-navigation/proguard-rules.pro b/wear/compose/compose-navigation/proguard-rules.pro
new file mode 100644
index 0000000..03b1b42
--- /dev/null
+++ b/wear/compose/compose-navigation/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We supply these as stubs and are able to link to them at runtime
+# because they are hidden public classes in Android. We don't want
+# R8 to complain about them not being there during optimization.
+
+# keep a small number of commonly used classes from being obfuscated so that we can determine if the
+# library is being used when apps are minified and obfuscated
+-keepnames class androidx.wear.compose.navigation.NavGraphBuilderKt
\ No newline at end of file
diff --git a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
index e343b99..5068468 100644
--- a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
@@ -1,4 +1,3 @@
-Landroidx/wear/compose/navigation/ComposableSingletons;
HSPLandroidx/wear/compose/navigation/NavGraphBuilderKt**->**(**)**
SPLandroidx/wear/compose/navigation/SwipeDismissableNavHostControllerKt**->**(**)**
HSPLandroidx/wear/compose/navigation/SwipeDismissableNavHostKt**->**(**)**
diff --git a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
index b14e334..1945520 100644
--- a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
+++ b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.test.performScrollToNode
import androidx.test.espresso.Espresso
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.wear.compose.integration.demos.Demo
import androidx.wear.compose.integration.demos.DemoActivity
@@ -49,6 +50,7 @@
@get:Rule
val rule = createAndroidComposeRule<DemoActivity>()
+ @FlakyTest(bugId = 259724403)
@Test
fun navigateThroughAllDemos() {
// Compose integration-tests are split into batches due to size,
diff --git a/wear/watchface/watchface-client/build.gradle b/wear/watchface/watchface-client/build.gradle
index 48e14cb..dcad002 100644
--- a/wear/watchface/watchface-client/build.gradle
+++ b/wear/watchface/watchface-client/build.gradle
@@ -62,9 +62,6 @@
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.wear.watchface.client"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
androidx {
@@ -73,4 +70,4 @@
mavenGroup = LibraryGroups.WEAR_WATCHFACE
inceptionYear = "2020"
description = "Client library for controlling androidx watchfaces"
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index dc02bd9..85af687 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -39,7 +39,6 @@
import androidx.test.screenshot.AndroidXScreenshotTestRule
import androidx.test.screenshot.assertAgainstGolden
import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.ComplicationSlotBoundsType
import androidx.wear.watchface.ContentDescriptionLabel
import androidx.wear.watchface.DrawMode
@@ -242,30 +241,6 @@
return value!!
}
- /**
- * Updates the complications for [interactiveInstance] and waits until they have been applied.
- */
- protected fun updateComplicationsBlocking(
- interactiveInstance: InteractiveWatchFaceClient,
- slotIdToComplicationData: Map<Int, ComplicationData>
- ) {
- val slotIdToWatchForUpdates = slotIdToComplicationData.keys.first()
- var slot: ComplicationSlot
-
- runBlocking {
- slot = engine.deferredWatchFaceImpl.await()
- .complicationSlotsManager.complicationSlots[slotIdToWatchForUpdates]!!
- }
-
- val updateCountDownLatch = CountDownLatch(1)
- handlerCoroutineScope.launch {
- slot.complicationData.collect { updateCountDownLatch.countDown() }
- }
-
- interactiveInstance.updateComplicationData(slotIdToComplicationData)
- assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
- }
-
protected fun tapOnComplication(
interactiveInstance: InteractiveWatchFaceClient,
slotId: Int
@@ -471,8 +446,7 @@
fun updateComplicationData() {
val interactiveInstance = getOrCreateTestSubject()
- updateComplicationsBlocking(
- interactiveInstance,
+ interactiveInstance.updateComplicationData(
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
rangedValueComplicationBuilder().build(),
@@ -1132,8 +1106,7 @@
surfaceHolder
)
val interactiveInstance = getOrCreateTestSubject(wallpaperService)
- updateComplicationsBlocking(
- interactiveInstance,
+ interactiveInstance.updateComplicationData(
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
rangedValueComplicationBuilder()
@@ -1194,8 +1167,7 @@
)
)
- updateComplicationsBlocking(
- interactiveInstance,
+ interactiveInstance.updateComplicationData(
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
timelineComplication.toApiComplicationData()
diff --git a/wear/watchface/watchface-complications-data-source/build.gradle b/wear/watchface/watchface-complications-data-source/build.gradle
index a60100a..32940e3 100644
--- a/wear/watchface/watchface-complications-data-source/build.gradle
+++ b/wear/watchface/watchface-complications-data-source/build.gradle
@@ -49,9 +49,6 @@
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.wear.watchface.complications.datasource"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
androidx {
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index ff7ac43..c473a58 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -41,7 +41,7 @@
field public static final androidx.wear.watchface.complications.data.ComplicationPersistencePolicies INSTANCE;
}
- public interface ComplicationText {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ComplicationText {
method public java.time.Instant getNextChangeTime(java.time.Instant afterInstant);
method public CharSequence getTextAt(android.content.res.Resources resources, java.time.Instant instant);
method public boolean isAlwaysEmpty();
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index c92a5a5..fdbd262 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -44,7 +44,7 @@
field public static final androidx.wear.watchface.complications.data.ComplicationPersistencePolicies INSTANCE;
}
- public interface ComplicationText {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ComplicationText {
method public java.time.Instant getNextChangeTime(java.time.Instant afterInstant);
method public CharSequence getTextAt(android.content.res.Resources resources, java.time.Instant instant);
method public boolean isAlwaysEmpty();
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index db4540b..83c88a5 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -41,7 +41,7 @@
field public static final androidx.wear.watchface.complications.data.ComplicationPersistencePolicies INSTANCE;
}
- public interface ComplicationText {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface ComplicationText {
method public java.time.Instant getNextChangeTime(java.time.Instant afterInstant);
method public CharSequence getTextAt(android.content.res.Resources resources, java.time.Instant instant);
method public boolean isAlwaysEmpty();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index 4b0bb1f..f335e6a 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -43,6 +43,7 @@
private typealias WireTimeDependentText = android.support.wearable.complications.TimeDependentText
+@JvmDefaultWithCompatibility
/**
* The text within a complication.
*
diff --git a/wear/watchface/watchface-editor/api/current.txt b/wear/watchface/watchface-editor/api/current.txt
index 817f024..a3e0615 100644
--- a/wear/watchface/watchface-editor/api/current.txt
+++ b/wear/watchface/watchface-editor/api/current.txt
@@ -34,7 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public androidx.wear.watchface.editor.EditorRequest createFromIntent(android.content.Intent intent) throws kotlinx.coroutines.TimeoutCancellationException;
}
- public interface EditorSession extends java.lang.AutoCloseable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface EditorSession extends java.lang.AutoCloseable {
method @RequiresApi(27) @UiThread public default static androidx.wear.watchface.editor.EditorSession createHeadlessEditorSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
method @UiThread @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public default static suspend Object? createOnWatchEditorSession(androidx.activity.ComponentActivity activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession>) throws kotlinx.coroutines.TimeoutCancellationException;
method public Integer? getBackgroundComplicationSlotId();
diff --git a/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt b/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt
index 817f024..a3e0615 100644
--- a/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-editor/api/public_plus_experimental_current.txt
@@ -34,7 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public androidx.wear.watchface.editor.EditorRequest createFromIntent(android.content.Intent intent) throws kotlinx.coroutines.TimeoutCancellationException;
}
- public interface EditorSession extends java.lang.AutoCloseable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface EditorSession extends java.lang.AutoCloseable {
method @RequiresApi(27) @UiThread public default static androidx.wear.watchface.editor.EditorSession createHeadlessEditorSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
method @UiThread @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public default static suspend Object? createOnWatchEditorSession(androidx.activity.ComponentActivity activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession>) throws kotlinx.coroutines.TimeoutCancellationException;
method public Integer? getBackgroundComplicationSlotId();
diff --git a/wear/watchface/watchface-editor/api/restricted_current.txt b/wear/watchface/watchface-editor/api/restricted_current.txt
index 817f024..a3e0615 100644
--- a/wear/watchface/watchface-editor/api/restricted_current.txt
+++ b/wear/watchface/watchface-editor/api/restricted_current.txt
@@ -34,7 +34,7 @@
method @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public androidx.wear.watchface.editor.EditorRequest createFromIntent(android.content.Intent intent) throws kotlinx.coroutines.TimeoutCancellationException;
}
- public interface EditorSession extends java.lang.AutoCloseable {
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface EditorSession extends java.lang.AutoCloseable {
method @RequiresApi(27) @UiThread public default static androidx.wear.watchface.editor.EditorSession createHeadlessEditorSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
method @UiThread @kotlin.jvm.Throws(exceptionClasses=TimeoutCancellationException::class) public default static suspend Object? createOnWatchEditorSession(androidx.activity.ComponentActivity activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession>) throws kotlinx.coroutines.TimeoutCancellationException;
method public Integer? getBackgroundComplicationSlotId();
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 699d7c2..bd12e2f 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -87,6 +87,7 @@
private const val TAG = "EditorSession"
+@JvmDefaultWithCompatibility
/**
* Interface for manipulating watch face state during a watch face editing session. The editor
* should adjust [userStyle] and call [openComplicationDataSourceChooser] to configure the watch
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 1d98429..d5c8fff 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -684,15 +684,20 @@
}
/**
- * At most one [UserStyleSetting.ComplicationSlotsUserStyleSetting] can be active at a time
- * based on the hierarchy of styles for any given [UserStyle]. This function finds the current
- * active [UserStyleSetting.ComplicationSlotsUserStyleSetting] based upon the [userStyle] and,
- * if there is one, it returns the corresponding selected [ComplicationSlotsOption]. Otherwise
- * it returns `null`.
+ * When a UserStyleSchema contains hierarchical styles, only part of it is deemed to be active
+ * based on the user’s options in [userStyle]. Conversely if the UserStyleSchema doesn’t contain
+ * any hierarchical styles then all of it is considered to be active all the time.
+ *
+ * From the active portion of the UserStyleSchema we only allow there to be at most one
+ * [UserStyleSetting.ComplicationSlotsUserStyleSetting]. This function searches the active
+ * portion of the UserStyleSchema for the [UserStyleSetting.ComplicationSlotsUserStyleSetting],
+ * if one is found then it returns the selected [ComplicationSlotsOption] from that, based on
+ * the [userStyle]. If a [UserStyleSetting.ComplicationSlotsUserStyleSetting] is not found in
+ * the active portion of the UserStyleSchema it returns `null`.
*
* @param userStyle The [UserStyle] for which the function will search for the selected
* [ComplicationSlotsOption], if any.
- * @return The selected [ComplicationSlotsOption] for the [userStyle] if any, or `null`
+ * @return The selected [ComplicationSlotsOption] based on the [userStyle] if any, or `null`
* otherwise.
*/
public fun findComplicationSlotsOptionForUserStyle(
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index d59f829..ddfc94c 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -70,9 +70,6 @@
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.wear.watchface"
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
}
androidx {
@@ -81,4 +78,4 @@
mavenGroup = LibraryGroups.WEAR_WATCHFACE
inceptionYear = "2020"
description = "Android Wear Watchface"
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 5e8f737..2eb58be 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -47,6 +47,7 @@
import android.view.SurfaceHolder
import android.view.WindowInsets
import android.view.accessibility.AccessibilityManager
+import androidx.annotation.AnyThread
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
@@ -102,6 +103,8 @@
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -347,10 +350,20 @@
public val XML_WATCH_FACE_METADATA =
"androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
- internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
- engine: WatchFaceService.EngineWrapper?,
+ internal enum class ExecutionThread {
+ UI,
+ CURRENT
+ }
+ /**
+ * Waits for deferredValue using runBlocking, then executes the task on the thread
+ * specified by executionThread param.
+ */
+ private fun <R, V> awaitDeferredThenRunTaskOnThread(
+ engine: EngineWrapper?,
traceName: String,
- task: (watchFaceImpl: WatchFaceImpl) -> R
+ executionThread: ExecutionThread,
+ task: (deferredValue: V) -> R,
+ waitDeferred: suspend (engine: EngineWrapper) -> V
): R? = TraceEvent(traceName).use {
if (engine == null) {
Log.w(TAG, "Task $traceName posted after close(), ignoring.")
@@ -359,9 +372,16 @@
runBlocking {
try {
withTimeout(AWAIT_DEFERRED_TIMEOUT) {
- val watchFaceImpl = engine.deferredWatchFaceImpl.await()
- withContext(engine.uiThreadCoroutineScope.coroutineContext) {
- task(watchFaceImpl)
+ val deferredValue = waitDeferred(engine)
+ when (executionThread) {
+ ExecutionThread.UI -> {
+ withContext(engine.uiThreadCoroutineScope.coroutineContext) {
+ task(deferredValue)
+ }
+ }
+ ExecutionThread.CURRENT -> {
+ task(deferredValue)
+ }
}
}
} catch (e: Exception) {
@@ -371,29 +391,24 @@
}
}
+ internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
+ engine: EngineWrapper?,
+ traceName: String,
+ task: (watchFaceImpl: WatchFaceImpl) -> R
+ ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+ it.deferredWatchFaceImpl.await()
+ }
+
/**
* During startup tasks will run before those posted by
* [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking].
*/
- internal fun <R> awaitDeferredWatchFaceThenRunOnBinderThread(
- engine: WatchFaceService.EngineWrapper?,
+ internal fun <R> awaitDeferredWatchFaceThenRunOnUiThread(
+ engine: EngineWrapper?,
traceName: String,
task: (watchFace: WatchFace) -> R
- ): R? = TraceEvent(traceName).use {
- if (engine == null) {
- Log.w(TAG, "Task $traceName posted after close(), ignoring.")
- return null
- }
- runBlocking {
- try {
- withTimeout(AWAIT_DEFERRED_TIMEOUT) {
- task(engine.deferredWatchFace.await())
- }
- } catch (e: Exception) {
- Log.e(TAG, "Operation $traceName failed", e)
- throw e
- }
- }
+ ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+ it.deferredWatchFace.await()
}
/**
@@ -401,25 +416,13 @@
* [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking] and
* [awaitDeferredWatchFaceThenRunOnBinderThread].
*/
- internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
- engine: WatchFaceService.EngineWrapper?,
+ internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnThread(
+ engine: EngineWrapper?,
traceName: String,
+ executionThread: ExecutionThread,
task: (earlyInitDetails: EarlyInitDetails) -> R
- ): R? = TraceEvent(traceName).use {
- if (engine == null) {
- Log.w(TAG, "Task $traceName ignored due to null engine.")
- return null
- }
- runBlocking {
- try {
- withTimeout(AWAIT_DEFERRED_TIMEOUT) {
- task(engine.deferredEarlyInitDetails.await())
- }
- } catch (e: Exception) {
- Log.e(TAG, "Operation $traceName failed", e)
- throw e
- }
- }
+ ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, executionThread, task) {
+ it.deferredEarlyInitDetails.await()
}
}
@@ -816,30 +819,31 @@
) = TraceEvent(
"WatchFaceService.writeComplicationCache"
).use {
- // File IO can be slow so perform the write from a background thread.
- getBackgroundThreadHandler().post {
- try {
- val stream = ByteArrayOutputStream()
- val objectOutputStream = ObjectOutputStream(stream)
- objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
- for (slot in complicationSlotsManager.complicationSlots) {
- objectOutputStream.writeInt(slot.key)
- objectOutputStream.writeObject(
- if ((slot.value.complicationData.value.persistencePolicy and
- ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
- ) {
- NoDataComplicationData().asWireComplicationData()
- } else {
- slot.value.complicationData.value.asWireComplicationData()
- }
- )
- }
- objectOutputStream.close()
- val byteArray = stream.toByteArray()
- writeComplicationDataCacheByteArray(context, fileName, byteArray)
- } catch (e: Exception) {
- Log.w(TAG, "Failed to write to complication cache due to exception", e)
+ try {
+ val stream = ByteArrayOutputStream()
+ val objectOutputStream = ObjectOutputStream(stream)
+ objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
+ for (slot in complicationSlotsManager.complicationSlots) {
+ objectOutputStream.writeInt(slot.key)
+ objectOutputStream.writeObject(
+ if ((slot.value.complicationData.value.persistencePolicy and
+ ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
+ ) {
+ NoDataComplicationData().asWireComplicationData()
+ } else {
+ slot.value.complicationData.value.asWireComplicationData()
+ }
+ )
}
+ objectOutputStream.close()
+ val byteArray = stream.toByteArray()
+
+ // File IO can be slow so perform the write from a background thread.
+ getBackgroundThreadHandler().post {
+ writeComplicationDataCacheByteArray(context, fileName, byteArray)
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to write to complication cache due to exception", e)
}
}
@@ -1134,8 +1138,7 @@
internal class EarlyInitDetails(
val complicationSlotsManager: ComplicationSlotsManager,
val userStyleRepository: CurrentUserStyleRepository,
- val userStyleFlavors: UserStyleFlavors,
- val surfaceHolder: SurfaceHolder
+ val userStyleFlavors: UserStyleFlavors
)
/** @hide */
@@ -1179,7 +1182,7 @@
* [deferredSurfaceHolder] will complete after [onSurfaceChanged], before then it's not
* safe to create a UiThread OpenGL context.
*/
- internal var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
+ private var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
internal val mutableWatchState = getMutableWatchState().apply {
isVisible.value = this@EngineWrapper.isVisible || forceIsVisibleForTesting()
@@ -1264,9 +1267,9 @@
private var asyncWatchFaceConstructionPending = false
- // Stores the initial ComplicationSlots which could get updated before they're applied.
- internal var pendingInitialComplications: List<IdAndComplicationDataWireFormat>? = null
- private set
+ @VisibleForTesting
+ internal val complicationsFlow =
+ MutableStateFlow<List<IdAndComplicationDataWireFormat>>(emptyList())
private var initialUserStyle: UserStyleWireFormat? = null
internal lateinit var interactiveInstanceId: String
@@ -1524,61 +1527,46 @@
}
}
- @UiThread
+ @AnyThread
internal fun setComplicationDataList(
complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
): Unit = TraceEvent("EngineWrapper.setComplicationDataList").use {
- val earlyInitDetails = getEarlyInitDetailsOrNull()
- if (earlyInitDetails != null) {
- applyComplications(
- earlyInitDetails.complicationSlotsManager,
- complicationDataWireFormats
- )
- } else {
- setPendingInitialComplications(complicationDataWireFormats)
- }
- }
-
- @UiThread
- internal fun applyComplications(
- complicationSlotsManager: ComplicationSlotsManager,
- complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
- ) {
- val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
- for (idAndComplicationData in complicationDataWireFormats) {
- complicationSlotsManager.onComplicationDataUpdate(
- idAndComplicationData.id,
- idAndComplicationData.complicationData.toApiComplicationData(),
- now
- )
- }
- complicationSlotsManager.onComplicationsUpdated()
- invalidate()
- scheduleWriteComplicationDataCache()
- }
-
- @UiThread
- internal fun setPendingInitialComplications(
- complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
- ) {
- // If the watchface hasn't been created yet, update pendingInitialComplications so
- // it can be applied later.
- if (pendingInitialComplications == null) {
- pendingInitialComplications = complicationDataWireFormats
- } else {
+ complicationsFlow.update { base ->
// We need to merge the updates.
- val complicationUpdateMap = pendingInitialComplications!!.associate {
+ val complicationUpdateMap = base.associate {
Pair(it.id, it.complicationData)
}.toMutableMap()
for (data in complicationDataWireFormats) {
complicationUpdateMap[data.id] = data.complicationData
}
- pendingInitialComplications = complicationUpdateMap.map {
+ complicationUpdateMap.map {
IdAndComplicationDataWireFormat(it.key, it.value)
}
}
}
+ @WorkerThread
+ private fun listenForComplicationChanges(
+ complicationSlotsManager: ComplicationSlotsManager
+ ) {
+ // Add a listener so we can track changes and automatically apply them on the UIThread
+ uiThreadCoroutineScope.launch {
+ complicationsFlow.collect { complicationDataWireFormats ->
+ val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
+ for (idAndComplicationData in complicationDataWireFormats) {
+ complicationSlotsManager.onComplicationDataUpdate(
+ idAndComplicationData.id,
+ idAndComplicationData.complicationData.toApiComplicationData(),
+ now
+ )
+ }
+ complicationSlotsManager.onComplicationsUpdated()
+ invalidate()
+ scheduleWriteComplicationDataCache()
+ }
+ }
+ }
+
@UiThread
internal suspend fun updateInstance(newInstanceId: String) {
val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1999,10 +1987,12 @@
// Store the initial complications, this could be modified by new data before being
// applied.
- pendingInitialComplications = params.idAndComplicationDataWireFormats
-
- if (pendingInitialComplications == null || pendingInitialComplications!!.isEmpty()) {
- pendingInitialComplications = readComplicationDataCache(_context, params.instanceId)
+ var initialComplications = params.idAndComplicationDataWireFormats
+ if (initialComplications.isNullOrEmpty()) {
+ initialComplications = readComplicationDataCache(_context, params.instanceId)
+ }
+ if (!initialComplications.isNullOrEmpty()) {
+ setComplicationDataList(initialComplications)
}
createWatchFaceInternal(
@@ -2038,6 +2028,15 @@
asyncWatchFaceConstructionPending = true
createdBy = _createdBy
+ // In case of overrideSurfaceHolder provided (tests) return its size instead of real
+ // metrics.
+ screenBounds = if (overrideSurfaceHolder != null) {
+ overrideSurfaceHolder.surfaceFrame
+ } else {
+ val displayMetrics = resources.displayMetrics
+ Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
+ }
+
backgroundThreadCoroutineScope.launch {
val timeBefore = System.currentTimeMillis()
val currentUserStyleRepository =
@@ -2052,12 +2051,21 @@
}
complicationSlotsManager.watchState = watchState
complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
+ listenForComplicationChanges(complicationSlotsManager)
val userStyleFlavors =
TraceEvent("WatchFaceService.createUserStyleFlavors").use {
createUserStyleFlavors(currentUserStyleRepository, complicationSlotsManager)
}
+ deferredEarlyInitDetails.complete(
+ EarlyInitDetails(
+ complicationSlotsManager,
+ currentUserStyleRepository,
+ userStyleFlavors
+ )
+ )
+
val deferredWatchFace = CompletableDeferred<WatchFace>()
val initComplicationsDone = CompletableDeferred<Unit>()
@@ -2076,24 +2084,6 @@
try {
val surfaceHolder = overrideSurfaceHolder ?: deferredSurfaceHolder.await()
- deferredEarlyInitDetails.complete(
- EarlyInitDetails(
- complicationSlotsManager,
- currentUserStyleRepository,
- userStyleFlavors,
- surfaceHolder
- )
- )
-
- withContext(uiThreadCoroutineScope.coroutineContext) {
- // Apply any pendingInitialComplications, this must be done after
- // deferredEarlyInitDetails has completed or there's a window in which complication
- // updates get lost.
- pendingInitialComplications?.let {
- applyComplications(complicationSlotsManager, it)
- }
- pendingInitialComplications = null
- }
val watchFace = TraceEvent("WatchFaceService.createWatchFace").use {
// Note by awaiting deferredSurfaceHolder we ensure onSurfaceChanged has
@@ -2678,6 +2668,9 @@
}
}
+ internal lateinit var screenBounds: Rect
+ private set
+
@UiThread
internal fun dump(writer: IndentingPrintWriter) {
require(uiThreadHandler.looper.isCurrentThread) {
@@ -2722,7 +2715,7 @@
writer.println("destroyed=$destroyed")
writer.println("surfaceDestroyed=$surfaceDestroyed")
writer.println(
- "pendingInitialComplications=" + pendingInitialComplications?.joinToString()
+ "lastComplications=" + complicationsFlow.value.joinToString()
)
synchronized(lock) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index badd1a8..a5f3897 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -85,11 +85,14 @@
"HeadlessWatchFaceImpl.getPreviewReferenceTimeMillis"
) { it.previewReferenceInstant.toEpochMilli() } ?: 0
- override fun getComplicationState() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
- engine,
- "HeadlessWatchFaceImpl.getComplicationState"
- ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+ override fun getComplicationState() = run {
+ val engineCopy = engine
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+ engineCopy,
+ "HeadlessWatchFaceImpl.getComplicationState",
+ WatchFaceService.Companion.ExecutionThread.UI
+ ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
+ }
override fun renderComplicationToBitmap(params: ComplicationRenderParams) =
WatchFaceService.awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
@@ -98,21 +101,24 @@
) { it.renderComplicationToBitmap(params) }
override fun getUserStyleSchema() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "HeadlessWatchFaceImpl.getUserStyleSchema"
+ "HeadlessWatchFaceImpl.getUserStyleSchema",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleRepository.schema.toWireFormat() }
override fun computeUserStyleSchemaDigestHash() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash"
+ "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleRepository.schema.getDigestHash() }
override fun getUserStyleFlavors() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "HeadlessWatchFaceImpl.getUserStyleFlavors"
+ "HeadlessWatchFaceImpl.getUserStyleFlavors",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleFlavors.toWireFormat() }
override fun release() {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 1eae0b4..ba6fce5 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -87,7 +87,7 @@
}
override fun getWatchFaceOverlayStyle(): WatchFaceOverlayStyleWireFormat? =
- WatchFaceService.awaitDeferredWatchFaceThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredWatchFaceThenRunOnUiThread(
engine,
"InteractiveWatchFaceImpl.getWatchFaceOverlayStyle"
) { WatchFaceOverlayStyleWireFormat(
@@ -160,9 +160,7 @@
override fun updateComplicationData(
complicationDatumWireFormats: MutableList<IdAndComplicationDataWireFormat>
- ): Unit = uiThreadCoroutineScope.runBlockingWithTracing(
- "InteractiveWatchFaceImpl.updateComplicationData"
- ) {
+ ): Unit = TraceEvent("InteractiveWatchFaceImpl.updateComplicationData").use {
if ("user" != Build.TYPE) {
Log.d(TAG, "updateComplicationData " + complicationDatumWireFormats.joinToString())
}
@@ -191,16 +189,19 @@
}
override fun getComplicationDetails(): List<IdAndComplicationStateWireFormat>? {
- return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
- engine,
- "InteractiveWatchFaceImpl.getComplicationDetails"
- ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+ val engineCopy = engine
+ return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+ engineCopy,
+ "InteractiveWatchFaceImpl.getComplicationDetails",
+ WatchFaceService.Companion.ExecutionThread.UI
+ ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
}
override fun getUserStyleSchema(): UserStyleSchemaWireFormat? {
- return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "InteractiveWatchFaceImpl.getUserStyleSchema"
+ "InteractiveWatchFaceImpl.getUserStyleSchema",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleRepository.schema.toWireFormat() }
}
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 97ca016..520e72f 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -5801,7 +5801,7 @@
}
@Test
- public fun setPendingInitialComplications() {
+ public fun setComplicationDataListMergesCorrectly() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -5829,15 +5829,17 @@
.build()
)
- engineWrapper.setPendingInitialComplications(listOf(left1))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1)
+ engineWrapper.setComplicationDataList(listOf(left1))
+ // In initEngine we fill initial complication data using
+ // setComplicationViaWallpaperCommand, that's why lastComplications initially is not empty
+ assertThat(engineWrapper.complicationsFlow.value).contains(left1)
// Check merges are working as expected.
- engineWrapper.setPendingInitialComplications(listOf(right))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1, right)
+ engineWrapper.setComplicationDataList(listOf(right))
+ assertThat(engineWrapper.complicationsFlow.value).containsExactly(left1, right)
- engineWrapper.setPendingInitialComplications(listOf(left2))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left2, right)
+ engineWrapper.setComplicationDataList(listOf(left2))
+ assertThat(engineWrapper.complicationsFlow.value).containsExactly(left2, right)
}
@Test
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-samples/src/main/AndroidManifest.xml
index 9c79acd..17431f8 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-samples/src/main/AndroidManifest.xml
@@ -191,7 +191,7 @@
android:configChanges="orientation|screenSize|screenLayout|screenSize"
android:label="@string/ime"/>
- <!-- ActivityEmbedding Initializer -->
+ <!-- Activity embedding initializer -->
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
@@ -202,7 +202,7 @@
android:value="androidx.startup" />
</provider>
- <!-- The app supports ActivityEmbedding itself, so it doesn't need system override. -->
+ <!-- The app itself supports activity embedding, so a system override is not needed. -->
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
android:value="false" />
diff --git a/window/window/build.gradle b/window/window/build.gradle
index f045acf..581da26 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -80,12 +80,6 @@
samples(project(":window:window-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-Xjvm-default=all"]
- }
-}
-
androidx {
name = "Jetpack WindowManager Library"
publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/window/window/src/androidTest/AndroidManifest.xml b/window/window/src/androidTest/AndroidManifest.xml
index af030c1..d543136 100644
--- a/window/window/src/androidTest/AndroidManifest.xml
+++ b/window/window/src/androidTest/AndroidManifest.xml
@@ -21,7 +21,7 @@
<activity android:name="androidx.window.TestConfigChangeHandlingActivity"
android:configChanges="orientation|screenLayout|screenSize"/>
- <!-- ActivityEmbedding Property -->
+ <!-- Activity embedding property -->
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
android:value="true" />
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index f661df6..226430b 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -17,25 +17,41 @@
package androidx.window
/**
- * Window related [android.content.pm.PackageManager.Property] tags for developers to define in app
- * AndroidManifest.
+ * Window-related
+ * [PackageManager.Property][android.content.pm.PackageManager.Property] tags
+ * that can be defined in the app manifest file, `AndroidManifest.xml`.
*/
object WindowProperties {
/**
- * Application level [android.content.pm.PackageManager.Property] tag for developers to
- * provide consent for their app to allow OEMs to manually provide ActivityEmbedding split
- * rule configuration on behalf of the app.
- * <p>If `true`, the system can override the windowing behaviors for the app, such as
- * showing some activities side-by-side. In this case, it will report that ActivityEmbedding
- * APIs are disabled for the app to avoid conflict.
- * <p>If `false`, the system can't override the window behavior for the app. It should
- * be used if the app wants to provide their own ActivityEmbedding split rules, or if the
- * app wants to opt-out of system overrides for any other reason.
- * <p>Default is `false`.
- * <p>The system enforcement is added in Android 14, but some devices may start following the
- * requirement before that. The best practice for apps is to always explicitly set this
- * property in AndroidManifest instead of relying on the default value.
- * <p>Example usage:
+ * Application-level
+ * [PackageManager.Property][android.content.pm.PackageManager.Property] tag
+ * that specifies whether OEMs are permitted to provide activity embedding
+ * split-rule configurations on behalf of the app.
+ *
+ * If `true`, the system is permitted to override the app's windowing
+ * behavior and implement activity embedding split rules, such as displaying
+ * activities side by side. A system override informs the app that the
+ * activity embedding APIs are disabled so the app will not provide its own
+ * activity embedding rules, which would conflict with the system's rules.
+ *
+ * If `false`, the system is not permitted to override the windowing
+ * behavior of the app. Set the property to `false` if the app provides its
+ * own activity embedding split rules, or if you want to prevent the system
+ * override for any other reason.
+ *
+ * The default value is `false`.
+ *
+ * <p class="note"><b>Note:</b> Refusal to permit the system override is not
+ * enforceable. OEMs can override the app's activity embedding
+ * implementation whether or not this property is specified and set to
+ * <code>false</code>. The property is, in effect, a hint to OEMs.</p>
+ *
+ * OEMs can implement activity embedding on any API level. The best practice
+ * for apps is to always explicitly set this property in the app manifest
+ * file regardless of targeted API level rather than rely on the default
+ * value.
+ *
+ * **Syntax:**
* <pre>
* <application>
* <property
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index d24c68a..d2e9c4c4 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -24,16 +24,26 @@
import kotlin.concurrent.withLock
/**
- * Controller class that will be used to get information about the currently active activity splits,
- * as well as provide interaction points to customize them and form new splits. A split is a pair of
- * containers that host activities in the same or different processes, combined under the same
- * parent window of the hosting task.
- * <p>A pair of activities can be put into split by providing a static or runtime split rule and
- * launching activity in the same task using [android.app.Activity.startActivity].
- * <p>This class is recommended to be configured in [androidx.startup.Initializer] or
- * [android.app.Application.onCreate], so that the rules are applied early in the application
- * startup before any activities complete initialization. The rule updates only apply to future
- * [android.app.Activity] launches and do not apply to already running activities.
+ * Controller class that gets information about the currently active activity
+ * splits and provides interaction points to customize the splits and form new
+ * splits.
+ *
+ * A split is a pair of containers that host activities in the same or different
+ * processes, combined under the same parent window of the hosting task.
+ *
+ * A pair of activities can be put into a split by providing a static or runtime
+ * split rule and then launching the activities in the same task using
+ * [Activity.startActivity()][android.app.Activity.startActivity].
+ *
+ * Configure this class with the Jetpack library
+ * [Initializer][androidx.startup.Initializer] or in
+ * [Application.onCreate()][android.app.Application.onCreate] so the rules are
+ * applied early in the application startup, before any activities complete
+ * initialization.
+ *
+ * Rule updates apply only to future activity launches, not to already running
+ * activities.
+ *
* @see initialize
*/
class SplitController private constructor(applicationContext: Context) {
@@ -114,14 +124,14 @@
}
/**
- * Indicates whether the split functionality is supported on the device. Note
- * that the device might enable splits in all conditions, but it should be
- * available in some states that the device supports. An example can be a
- * foldable device with multiple screens can choose to collapse all splits for
- * apps running on a small display, but enable when running on a larger
- * one - on such devices this method will always return "true".
- * If the split is not supported, activities will be launched on top, following
- * the regular model.
+ * Indicates whether split functionality is supported on the device. Note
+ * that devices might not enable splits in all states or conditions. For
+ * example, a foldable device with multiple screens can choose to collapse
+ * splits when apps run on the device's small display, but enable splits
+ * when apps run on the device's large display. In cases like this,
+ * `isSplitSupported` always returns `true`, and if the split is collapsed,
+ * activities are launched on top, following the non-activity embedding
+ * model.
*/
fun isSplitSupported(): Boolean {
return embeddingBackend.isSplitSupported()
diff --git a/work/work-runtime/api/current.ignore b/work/work-runtime/api/current.ignore
new file mode 100644
index 0000000..33b7b2f
--- /dev/null
+++ b/work/work-runtime/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedType: androidx.work.WorkInfo#getTags():
+ Method androidx.work.WorkInfo.getTags has changed return type from java.util.Set<java.lang.String!> to java.util.Set<java.lang.String>
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 446ee00..370e634 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -320,13 +320,23 @@
method public java.util.UUID getId();
method public androidx.work.Data getOutputData();
method public androidx.work.Data getProgress();
- method @IntRange(from=0) public int getRunAttemptCount();
+ method @IntRange(from=0L) public int getRunAttemptCount();
method public androidx.work.WorkInfo.State getState();
- method public java.util.Set<java.lang.String!> getTags();
+ method public java.util.Set<java.lang.String> getTags();
+ property public final int generation;
+ property public final java.util.UUID id;
+ property public final androidx.work.Data outputData;
+ property public final androidx.work.Data progress;
+ property @IntRange(from=0L) public final int runAttemptCount;
+ property public final androidx.work.WorkInfo.State state;
+ property public final java.util.Set<java.lang.String> tags;
}
public enum WorkInfo.State {
- method public boolean isFinished();
+ method public final boolean isFinished();
+ method public static androidx.work.WorkInfo.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.work.WorkInfo.State[] values();
+ property public final boolean isFinished;
enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
diff --git a/work/work-runtime/api/public_plus_experimental_current.txt b/work/work-runtime/api/public_plus_experimental_current.txt
index 446ee00..370e634 100644
--- a/work/work-runtime/api/public_plus_experimental_current.txt
+++ b/work/work-runtime/api/public_plus_experimental_current.txt
@@ -320,13 +320,23 @@
method public java.util.UUID getId();
method public androidx.work.Data getOutputData();
method public androidx.work.Data getProgress();
- method @IntRange(from=0) public int getRunAttemptCount();
+ method @IntRange(from=0L) public int getRunAttemptCount();
method public androidx.work.WorkInfo.State getState();
- method public java.util.Set<java.lang.String!> getTags();
+ method public java.util.Set<java.lang.String> getTags();
+ property public final int generation;
+ property public final java.util.UUID id;
+ property public final androidx.work.Data outputData;
+ property public final androidx.work.Data progress;
+ property @IntRange(from=0L) public final int runAttemptCount;
+ property public final androidx.work.WorkInfo.State state;
+ property public final java.util.Set<java.lang.String> tags;
}
public enum WorkInfo.State {
- method public boolean isFinished();
+ method public final boolean isFinished();
+ method public static androidx.work.WorkInfo.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.work.WorkInfo.State[] values();
+ property public final boolean isFinished;
enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
diff --git a/work/work-runtime/api/restricted_current.ignore b/work/work-runtime/api/restricted_current.ignore
new file mode 100644
index 0000000..33b7b2f
--- /dev/null
+++ b/work/work-runtime/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedType: androidx.work.WorkInfo#getTags():
+ Method androidx.work.WorkInfo.getTags has changed return type from java.util.Set<java.lang.String!> to java.util.Set<java.lang.String>
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 446ee00..370e634 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -320,13 +320,23 @@
method public java.util.UUID getId();
method public androidx.work.Data getOutputData();
method public androidx.work.Data getProgress();
- method @IntRange(from=0) public int getRunAttemptCount();
+ method @IntRange(from=0L) public int getRunAttemptCount();
method public androidx.work.WorkInfo.State getState();
- method public java.util.Set<java.lang.String!> getTags();
+ method public java.util.Set<java.lang.String> getTags();
+ property public final int generation;
+ property public final java.util.UUID id;
+ property public final androidx.work.Data outputData;
+ property public final androidx.work.Data progress;
+ property @IntRange(from=0L) public final int runAttemptCount;
+ property public final androidx.work.WorkInfo.State state;
+ property public final java.util.Set<java.lang.String> tags;
}
public enum WorkInfo.State {
- method public boolean isFinished();
+ method public final boolean isFinished();
+ method public static androidx.work.WorkInfo.State valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.work.WorkInfo.State[] values();
+ property public final boolean isFinished;
enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkInfo.java b/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
deleted file mode 100644
index 7560af3..0000000
--- a/work/work-runtime/src/main/java/androidx/work/WorkInfo.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-
-/**
- * Information about a particular {@link WorkRequest} containing the id of the WorkRequest, its
- * current {@link State}, output, tags, and run attempt count. Note that output is only available
- * for the terminal states ({@link State#SUCCEEDED} and {@link State#FAILED}).
- */
-
-public final class WorkInfo {
-
- private @NonNull UUID mId;
- private @NonNull State mState;
- private @NonNull Data mOutputData;
- private @NonNull Set<String> mTags;
- private @NonNull Data mProgress;
- private int mRunAttemptCount;
-
- private final int mGeneration;
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public WorkInfo(
- @NonNull UUID id,
- @NonNull State state,
- @NonNull Data outputData,
- @NonNull List<String> tags,
- @NonNull Data progress,
- int runAttemptCount,
- int generation) {
- mId = id;
- mState = state;
- mOutputData = outputData;
- mTags = new HashSet<>(tags);
- mProgress = progress;
- mRunAttemptCount = runAttemptCount;
- mGeneration = generation;
- }
-
- /**
- * Gets the identifier of the {@link WorkRequest}.
- *
- * @return The identifier of a {@link WorkRequest}
- */
- public @NonNull UUID getId() {
- return mId;
- }
-
- /**
- * Gets the current {@link State} of the {@link WorkRequest}.
- *
- * @return The current {@link State} of the {@link WorkRequest}
- */
- public @NonNull State getState() {
- return mState;
- }
-
- /**
- * Gets the output {@link Data} for the {@link WorkRequest}. If the WorkRequest is unfinished,
- * this is always {@link Data#EMPTY}.
- *
- * @return The output {@link Data} of the {@link WorkRequest}
- */
- public @NonNull Data getOutputData() {
- return mOutputData;
- }
-
- /**
- * Gets the {@link Set} of tags associated with the {@link WorkRequest}.
- *
- * @return The {@link Set} of tags associated with the {@link WorkRequest}
- */
- public @NonNull Set<String> getTags() {
- return mTags;
- }
-
- /**
- * Gets the progress {@link Data} associated with the {@link WorkRequest}.
- *
- * @return The progress {@link Data} associated with the {@link WorkRequest}
- */
- public @NonNull Data getProgress() {
- return mProgress;
- }
-
- /**
- * Gets the run attempt count of the {@link WorkRequest}. Note that for
- * {@link PeriodicWorkRequest}s, the run attempt count gets reset between successful runs.
- *
- * @return The run attempt count of the {@link WorkRequest}.
- */
- @IntRange(from = 0)
- public int getRunAttemptCount() {
- return mRunAttemptCount;
- }
-
- /**
- * Gets the latest generation of this Worker.
- * <p>
- * A work has multiple generations, if it was updated via
- * {@link WorkManager#updateWork(WorkRequest)} or
- * {@link WorkManager#enqueueUniquePeriodicWork(String,
- * ExistingPeriodicWorkPolicy, PeriodicWorkRequest)} using
- * {@link ExistingPeriodicWorkPolicy#UPDATE}.
- * <p>
- * If this worker is currently running, it can possibly be of an older generation rather than
- * returned by this function if an update has happened during an execution of this worker.
- *
- * @return a generation of this work.
- */
- public int getGeneration() {
- return mGeneration;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- WorkInfo workInfo = (WorkInfo) o;
-
- if (mRunAttemptCount != workInfo.mRunAttemptCount) return false;
- if (mGeneration != workInfo.mGeneration) return false;
- if (!mId.equals(workInfo.mId)) return false;
- if (mState != workInfo.mState) return false;
- if (!mOutputData.equals(workInfo.mOutputData)) return false;
- if (!mTags.equals(workInfo.mTags)) return false;
- return mProgress.equals(workInfo.mProgress);
- }
-
- @Override
- public int hashCode() {
- int result = mId.hashCode();
- result = 31 * result + mState.hashCode();
- result = 31 * result + mOutputData.hashCode();
- result = 31 * result + mTags.hashCode();
- result = 31 * result + mProgress.hashCode();
- result = 31 * result + mRunAttemptCount;
- result = 31 * result + mGeneration;
- return result;
- }
-
- @Override
- public String toString() {
- return "WorkInfo{"
- + "mId='" + mId + '\''
- + ", mState=" + mState
- + ", mOutputData=" + mOutputData
- + ", mTags=" + mTags
- + ", mProgress=" + mProgress
- + '}';
- }
-
- /**
- * The current lifecycle state of a {@link WorkRequest}.
- */
- public enum State {
-
- /**
- * Used to indicate that the {@link WorkRequest} is enqueued and eligible to run when its
- * {@link Constraints} are met and resources are available.
- */
- ENQUEUED,
-
- /**
- * Used to indicate that the {@link WorkRequest} is currently being executed.
- */
- RUNNING,
-
- /**
- * Used to indicate that the {@link WorkRequest} has completed in a successful state. Note
- * that {@link PeriodicWorkRequest}s will never enter this state (they will simply go back
- * to {@link #ENQUEUED} and be eligible to run again).
- */
- SUCCEEDED,
-
- /**
- * Used to indicate that the {@link WorkRequest} has completed in a failure state. All
- * dependent work will also be marked as {@code #FAILED} and will never run.
- */
- FAILED,
-
- /**
- * Used to indicate that the {@link WorkRequest} is currently blocked because its
- * prerequisites haven't finished successfully.
- */
- BLOCKED,
-
- /**
- * Used to indicate that the {@link WorkRequest} has been cancelled and will not execute.
- * All dependent work will also be marked as {@code #CANCELLED} and will not run.
- */
- CANCELLED;
-
- /**
- * Returns {@code true} if this State is considered finished.
- *
- * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and * {@link #CANCELLED}
- * states
- */
- public boolean isFinished() {
- return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
- }
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt b/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
new file mode 100644
index 0000000..5d4ab92
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work
+
+import androidx.annotation.IntRange
+import androidx.annotation.RestrictTo
+import androidx.work.WorkInfo.State
+import java.util.UUID
+
+/**
+ * Information about a particular [WorkRequest] containing the id of the WorkRequest, its
+ * current [State], output, tags, and run attempt count. Note that output is only available
+ * for the terminal states ([State.SUCCEEDED] and [State.FAILED]).
+ */
+class WorkInfo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
+ /**
+ * The identifier of the [WorkRequest].
+ */
+ val id: UUID,
+ /**
+ * The current [State] of the [WorkRequest].
+ */
+ val state: State,
+ /**
+ * The output [Data] for the [WorkRequest]. If the WorkRequest is unfinished,
+ * this is always [Data.EMPTY].
+ */
+ val outputData: Data,
+
+ tags: List<String>,
+ /**
+ * The progress [Data] associated with the [WorkRequest].
+ */
+ val progress: Data,
+
+ /**
+ * The run attempt count of the [WorkRequest]. Note that for
+ * [PeriodicWorkRequest]s, the run attempt count gets reset between successful runs.
+ */
+ @get:IntRange(from = 0)
+ val runAttemptCount: Int,
+
+ /**
+ * The latest generation of this Worker.
+ *
+ *
+ * A work has multiple generations, if it was updated via
+ * [WorkManager.updateWork] or
+ * [WorkManager.enqueueUniquePeriodicWork] using
+ * [ExistingPeriodicWorkPolicy.UPDATE].
+ *
+ *
+ * If this worker is currently running, it can possibly be of an older generation rather than
+ * returned by this function if an update has happened during an execution of this worker.
+ */
+ val generation: Int
+) {
+ /**
+ * The [Set] of tags associated with the [WorkRequest].
+ */
+ val tags: Set<String> = HashSet(tags)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+ val workInfo = other as WorkInfo
+ if (runAttemptCount != workInfo.runAttemptCount) return false
+ if (generation != workInfo.generation) return false
+ if (id != workInfo.id) return false
+ if (state != workInfo.state) return false
+ if (outputData != workInfo.outputData) return false
+ return if (tags != workInfo.tags) false else progress == workInfo.progress
+ }
+
+ override fun hashCode(): Int {
+ var result = id.hashCode()
+ result = 31 * result + state.hashCode()
+ result = 31 * result + outputData.hashCode()
+ result = 31 * result + tags.hashCode()
+ result = 31 * result + progress.hashCode()
+ result = 31 * result + runAttemptCount
+ result = 31 * result + generation
+ return result
+ }
+
+ override fun toString(): String {
+ return ("WorkInfo{mId='$id', mState=$state, " +
+ "mOutputData=$outputData, mTags=$tags, mProgress=$progress}")
+ }
+
+ /**
+ * The current lifecycle state of a [WorkRequest].
+ */
+ enum class State {
+ /**
+ * Used to indicate that the [WorkRequest] is enqueued and eligible to run when its
+ * [Constraints] are met and resources are available.
+ */
+ ENQUEUED,
+
+ /**
+ * Used to indicate that the [WorkRequest] is currently being executed.
+ */
+ RUNNING,
+
+ /**
+ * Used to indicate that the [WorkRequest] has completed in a successful state. Note
+ * that [PeriodicWorkRequest]s will never enter this state (they will simply go back
+ * to [.ENQUEUED] and be eligible to run again).
+ */
+ SUCCEEDED,
+
+ /**
+ * Used to indicate that the [WorkRequest] has completed in a failure state. All
+ * dependent work will also be marked as `#FAILED` and will never run.
+ */
+ FAILED,
+
+ /**
+ * Used to indicate that the [WorkRequest] is currently blocked because its
+ * prerequisites haven't finished successfully.
+ */
+ BLOCKED,
+
+ /**
+ * Used to indicate that the [WorkRequest] has been cancelled and will not execute.
+ * All dependent work will also be marked as `#CANCELLED` and will not run.
+ */
+ CANCELLED;
+
+ /**
+ * Returns `true` if this State is considered finished:
+ * [.SUCCEEDED], [.FAILED], and * [.CANCELLED]
+ */
+ val isFinished: Boolean
+ get() = this == SUCCEEDED || this == FAILED || this == CANCELLED
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt
index 47b9e55..b07aeca 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/SystemIdInfoDao.kt
@@ -20,6 +20,7 @@
import androidx.room.OnConflictStrategy
import androidx.room.Query
+@JvmDefaultWithCompatibility
/**
* A Data Access Object for [SystemIdInfo].
*/
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt
index db0be48..2828c61f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTagDao.kt
@@ -20,6 +20,7 @@
import androidx.room.OnConflictStrategy
import androidx.room.Query
+@JvmDefaultWithCompatibility
/**
* The Data Access Object for [WorkTag]s.
*/