Merge "Disable failing OverscrollBenchmark#overscroll_* benchmarks" into androidx-main
diff --git a/OWNERS b/OWNERS
index 0393b15..89b9afc 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,6 +4,7 @@
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
index bfcd4e1..c3f7793 100644
--- a/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
+++ b/activity/activity-ktx/src/androidTest/java/androidx/activity/ActivityViewModelLazyTest.kt
@@ -62,15 +62,15 @@
super.onCreate(savedInstanceState)
}
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return SavedStateViewModelFactory()
- }
+ override val defaultViewModelProviderFactory
+ get() = SavedStateViewModelFactory()
- override fun getDefaultViewModelCreationExtras(): CreationExtras {
- val extras = MutableCreationExtras(super.getDefaultViewModelCreationExtras())
- extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
- return extras
- }
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() {
+ val extras = MutableCreationExtras(super.defaultViewModelCreationExtras)
+ extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+ return extras
+ }
}
class TestViewModel : ViewModel()
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index d19ee81..9f5dbc4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -47,11 +47,9 @@
import androidx.camera.core.impl.utils.futures.Futures
import com.google.common.util.concurrent.ListenableFuture
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.launch
/**
* Adapt the [CameraControlInternal] interface to [CameraPipe].
@@ -118,19 +116,8 @@
)
}
- override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
- return threads.scope.launch(start = CoroutineStart.UNDISPATCHED) {
- useCaseManager.camera?.let {
- zoomControl.zoomRatio = ratio
- val zoomValue = ZoomValue(
- ratio,
- zoomControl.minZoom,
- zoomControl.maxZoom
- )
- cameraControlStateAdapter.setZoomState(zoomValue)
- }
- }.asListenableFuture()
- }
+ override fun setZoomRatio(ratio: Float): ListenableFuture<Void> =
+ zoomControl.setZoomRatioAsync(ratio)
override fun setLinearZoom(linearZoom: Float): ListenableFuture<Void> {
val ratio = zoomControl.toZoomRatio(linearZoom)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
index 6e3e98c..6abf608 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
@@ -25,10 +25,7 @@
import androidx.camera.core.ExposureState
import androidx.camera.core.ZoomState
import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
/**
* [CameraControlStateAdapter] caches and updates based on callbacks from the active CameraGraph.
@@ -44,24 +41,9 @@
val torchStateLiveData: LiveData<Int>
get() = torchControl.torchStateLiveData
- private val _zoomState by lazy {
- MutableLiveData<ZoomState>(
- ZoomValue(
- zoomControl.zoomRatio,
- zoomControl.minZoom,
- zoomControl.maxZoom
- )
- )
- }
val zoomStateLiveData: LiveData<ZoomState>
- get() = _zoomState
-
- suspend fun setZoomState(value: ZoomState) {
- withContext(Dispatchers.Main) {
- _zoomState.value = value
- }
- }
+ get() = zoomControl.zoomStateLiveData
val exposureState: ExposureState
get() = evCompControl.exposureState
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
index 7a9a5be..844552b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
@@ -17,31 +17,48 @@
package androidx.camera.camera2.pipe.integration.impl
import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.adapter.ZoomValue
+import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.ZoomState
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.google.common.util.concurrent.ListenableFuture
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
import javax.inject.Inject
+import kotlin.math.abs
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+const val DEFAULT_ZOOM_RATIO = 1.0f
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@CameraScope
-class ZoomControl @Inject constructor(private val zoomCompat: ZoomCompat) : UseCaseCameraControl {
- private var _zoomRatio = 1.0f
- var zoomRatio: Float
- get() = _zoomRatio
- set(value) {
- // TODO: Make this a suspend function?
- _zoomRatio = value
- update()
- }
-
+class ZoomControl @Inject constructor(
+ private val threads: UseCaseThreads,
+ private val zoomCompat: ZoomCompat,
+) : UseCaseCameraControl {
// NOTE: minZoom may be lower than 1.0
- // NOTE: Default zoom ratio is 1.0
- // NOTE: Linear zoom is
+ // NOTE: Default zoom ratio is 1.0 (DEFAULT_ZOOM_RATIO)
val minZoom: Float = zoomCompat.minZoom
val maxZoom: Float = zoomCompat.maxZoom
+ val defaultZoomState by lazy {
+ ZoomValue(DEFAULT_ZOOM_RATIO, minZoom, maxZoom)
+ }
+
+ private val _zoomState by lazy {
+ MutableLiveData<ZoomState>(defaultZoomState)
+ }
+
+ val zoomStateLiveData: LiveData<ZoomState>
+ get() = _zoomState
+
/** Linear zoom is between 0.0f and 1.0f */
fun toLinearZoom(zoomRatio: Float): Float {
val range = zoomCompat.maxZoom - zoomCompat.minZoom
@@ -57,7 +74,13 @@
if (range > 0) {
return linearZoom * range + zoomCompat.minZoom
}
- return 1.0f
+
+ // if minZoom = maxZoom = 2.0f, 2.0f should be returned instead of default 1.0f
+ if (nearZero(range)) {
+ return zoomCompat.minZoom
+ }
+
+ return DEFAULT_ZOOM_RATIO
}
private var _useCaseCamera: UseCaseCamera? = null
@@ -69,17 +92,48 @@
}
override fun reset() {
- // TODO: 1.0 may not be a reasonable value to reset the zoom state too.
- zoomRatio = 1.0f
+ // TODO: 1.0 may not be a reasonable value to reset the zoom state to.
+ threads.sequentialScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ setZoomState(defaultZoomState)
+ }
+
update()
}
private fun update() {
_useCaseCamera?.let {
- zoomCompat.apply(_zoomRatio, it)
+ zoomCompat.apply(_zoomState.value?.zoomRatio ?: DEFAULT_ZOOM_RATIO, it)
}
}
+ private suspend fun setZoomState(value: ZoomState) {
+ // TODO: camera-camera2 updates livedata with setValue if calling thread is main thread,
+ // and updates with postValue otherwise. Need to consider if always using setValue
+ // via main thread is alright in camera-pipe.
+ withContext(Dispatchers.Main) {
+ _zoomState.value = value
+ }
+ }
+
+ fun setZoomRatioAsync(ratio: Float): ListenableFuture<Void> {
+ // TODO: report IllegalArgumentException if ratio not in range
+ return threads.sequentialScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ useCaseCamera?.let {
+ val zoomValue = ZoomValue(
+ ratio,
+ minZoom,
+ maxZoom
+ )
+ setZoomState(zoomValue)
+ update()
+ }
+ }.asListenableFuture()
+ }
+
+ private fun nearZero(num: Float): Boolean {
+ return abs(num) < 2.0 * Math.ulp(abs(num))
+ }
+
@Module
abstract class Bindings {
@Binds
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
index 6432d83..fe14c55 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
@@ -18,12 +18,23 @@
import android.os.Build
import android.util.Size
+import androidx.camera.camera2.pipe.integration.impl.ZoomControl
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.createCameraInfoAdapter
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.useCaseThreads
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
+import androidx.camera.core.ZoomState
import androidx.camera.core.impl.ImageFormatConstants
+import androidx.testutils.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@@ -33,7 +44,11 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraInfoAdapterTest {
- private val cameraInfoAdapter = createCameraInfoAdapter()
+ private val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+ private val cameraInfoAdapter = createCameraInfoAdapter(zoomControl = zoomControl)
+
+ @get:Rule
+ val dispatcherRule = MainDispatcherRule(MoreExecutors.directExecutor().asCoroutineDispatcher())
@Test
fun getSupportedResolutions() {
@@ -61,4 +76,51 @@
.that(cameraInfoAdapter.isFocusMeteringSupported(action))
.isAnyOf(true, false)
}
+
+ @Test
+ fun canReturnDefaultZoomState() {
+ // make new ZoomControl to test first-time initialization scenario
+ val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+ val cameraInfoAdapter = createCameraInfoAdapter(zoomControl = zoomControl)
+
+ assertWithMessage("zoomState did not return default zoom ratio successfully")
+ .that(cameraInfoAdapter.zoomState.value)
+ .isEqualTo(zoomControl.defaultZoomState)
+ }
+
+ @Test
+ fun canObserveZoomStateUpdate(): Unit = runBlocking {
+ var currentZoomState: ZoomState = ZoomValue(-1.0f, -1.0f, -1.0f)
+ cameraInfoAdapter.zoomState.observeForever {
+ currentZoomState = it
+ }
+
+ // if useCaseCamera is null, zoom setting operation will be cancelled
+ zoomControl.useCaseCamera = FakeUseCaseCamera()
+
+ zoomControl.setZoomRatioAsync(3.0f)[3, TimeUnit.SECONDS]
+
+ // minZoom and maxZoom will be set as 0 due to FakeZoomCompat using those values
+ assertWithMessage("zoomState did not return default zoom ratio successfully")
+ .that(currentZoomState)
+ .isEqualTo(ZoomValue(3.0f, zoomControl.minZoom, zoomControl.maxZoom))
+ }
+
+ @Test
+ fun canObserveZoomStateReset(): Unit = runBlocking {
+ var currentZoomState: ZoomState = ZoomValue(-1.0f, -1.0f, -1.0f)
+ cameraInfoAdapter.zoomState.observeForever {
+ currentZoomState = it
+ }
+
+ // if useCaseCamera is null, zoom setting operation will be cancelled
+ zoomControl.useCaseCamera = FakeUseCaseCamera()
+
+ zoomControl.reset()
+
+ // minZoom and maxZoom will be set as 0 due to FakeZoomCompat using those values
+ assertWithMessage("zoomState did not return default zoom state successfully")
+ .that(currentZoomState)
+ .isEqualTo(zoomControl.defaultZoomState)
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
new file mode 100644
index 0000000..600f8ab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
+import androidx.testutils.MainDispatcherRule
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@DoNotInstrument
+class ZoomControlTest {
+ @get:Rule
+ val dispatcherRule = MainDispatcherRule(MoreExecutors.directExecutor().asCoroutineDispatcher())
+
+ private val fakeUseCaseThreads by lazy {
+ val executor = Executors.newSingleThreadExecutor()
+ val dispatcher = executor.asCoroutineDispatcher()
+ val cameraScope = CoroutineScope(Job() + dispatcher)
+
+ UseCaseThreads(
+ cameraScope,
+ executor,
+ dispatcher,
+ )
+ }
+
+ private val zoomCompat = FakeZoomCompat()
+ private lateinit var zoomControl: ZoomControl
+
+ @Before
+ fun setUp() {
+ zoomControl = ZoomControl(fakeUseCaseThreads, zoomCompat).apply {
+ useCaseCamera = FakeUseCaseCamera()
+ }
+ }
+
+ @Test
+ fun canUpdateZoomRatioInCompat() {
+ zoomControl.setZoomRatioAsync(3.0f)[3, TimeUnit.SECONDS]
+
+ Truth.assertWithMessage("zoomState did not return default zoom state successfully")
+ .that(zoomCompat.zoomRatio)
+ .isEqualTo(3.0f)
+ }
+
+ // TODO: port tests from camera-camera2
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 34ce448..5c6e224 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -32,7 +32,6 @@
import androidx.camera.camera2.pipe.integration.impl.TorchControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.impl.ZoomControl
-import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.core.impl.ImageFormatConstants
import com.google.common.util.concurrent.MoreExecutors
@@ -45,7 +44,7 @@
object FakeCameraInfoAdapterCreator {
private val CAMERA_ID_0 = CameraId("0")
- private val useCaseThreads by lazy {
+ val useCaseThreads by lazy {
val executor = MoreExecutors.directExecutor()
val dispatcher = executor.asCoroutineDispatcher()
val cameraScope = CoroutineScope(
@@ -68,7 +67,8 @@
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE to Rect(0, 0, 640, 480)
)
- @OptIn(ExperimentalCamera2Interop::class)
+ private val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+
fun createCameraInfoAdapter(
cameraId: CameraId = CAMERA_ID_0,
cameraProperties: CameraProperties = FakeCameraProperties(
@@ -77,13 +77,14 @@
characteristics = cameraCharacteristics
),
cameraId
- )
+ ),
+ zoomControl: ZoomControl = this.zoomControl,
) = CameraInfoAdapter(
cameraProperties,
CameraConfig(cameraId),
CameraStateAdapter(),
CameraControlStateAdapter(
- ZoomControl(FakeZoomCompat()),
+ zoomControl,
EvCompControl(FakeEvCompCompat()),
TorchControl(cameraProperties, useCaseThreads),
),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
index d2f88f5..cabac1a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
@@ -23,8 +23,9 @@
override val minZoom: Float = 0f,
override val maxZoom: Float = 0f,
) : ZoomCompat {
+ var zoomRatio = 0f
override fun apply(zoomRatio: Float, camera: UseCaseCamera) {
- TODO("Not yet implemented")
+ this.zoomRatio = zoomRatio
}
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
index 397e40d..ecb3a96 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCameraManager.kt
@@ -248,7 +248,14 @@
timeout = 1000,
callback = {
channel.trySend(RequestClose(this)).isSuccess
- }
+ },
+ // Every ActiveCamera is associated with an opened camera. We should ensure that we
+ // issue a RequestClose eventually for every ActiveCamera created.
+ //
+ // A notable bug is b/264396089 where, because camera opens took too long, we didn't
+ // acquire a WakeLockToken, and thereby not issuing the request to close camera
+ // eventually.
+ startTimeoutOnCreation = true
)
init {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt
index b6e08bf..adfb1c5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/WakeLock.kt
@@ -38,7 +38,8 @@
internal class WakeLock(
private val scope: CoroutineScope,
private val timeout: Long = 0,
- private val callback: () -> Unit
+ private val startTimeoutOnCreation: Boolean = false,
+ private val callback: () -> Unit,
) {
private val lock = Any()
@@ -51,6 +52,14 @@
@GuardedBy("lock")
private var closed = false
+ init {
+ if (startTimeoutOnCreation) {
+ synchronized(lock) {
+ startTimeout()
+ }
+ }
+ }
+
private inner class WakeLockToken : Token {
private val closed = atomic(false)
override fun release(): Boolean {
@@ -99,21 +108,26 @@
synchronized(lock) {
count -= 1
if (count == 0 && !closed) {
- timeoutJob = scope.launch {
- delay(timeout)
-
- synchronized(lock) {
- if (closed || count != 0) {
- return@launch
- }
- timeoutJob = null
- closed = true
- }
-
- // Execute the callback
- callback()
- }
+ startTimeout()
}
}
}
+
+ @GuardedBy("lock")
+ private fun startTimeout() {
+ timeoutJob = scope.launch {
+ delay(timeout)
+
+ synchronized(lock) {
+ if (closed || count != 0) {
+ return@launch
+ }
+ timeoutJob = null
+ closed = true
+ }
+
+ // Execute the callback
+ callback()
+ }
+ }
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt
index 993d7a1..a22b591 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/WakeLockTest.kt
@@ -70,4 +70,13 @@
wakelock.release()
assertThat(result.await()).isTrue()
}
+
+ @Test
+ fun testWakeLockCompletesWhenStartTimeoutOnCreation() = runTest {
+ val result = CompletableDeferred<Boolean>()
+ WakeLock(this, 100, startTimeoutOnCreation = true) {
+ result.complete(true)
+ }
+ assertThat(result.await()).isTrue()
+ }
}
\ No newline at end of file
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 dfb71ec..b8c1845 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
@@ -126,7 +126,7 @@
Size inputSize = input.getSize();
Rect cropRect = outConfig.getCropRect();
int rotationDegrees = input.getRotationDegrees();
- boolean mirroring = input.getMirroring();
+ boolean mirroring = outConfig.getMirroring();
// Calculate sensorToBufferTransform
android.graphics.Matrix sensorToBufferTransform =
@@ -149,7 +149,7 @@
// Crop rect is always the full size.
sizeToRect(outConfig.getSize()),
/*rotationDegrees=*/0,
- /*mirroring=*/false);
+ /*mirroring=*/input.getMirroring() != mirroring);
return outputSurface;
}
@@ -194,7 +194,7 @@
input.getSize(),
output.getKey().getCropRect(),
input.getRotationDegrees(),
- input.getMirroring());
+ output.getKey().getMirroring());
Futures.addCallback(future, new FutureCallback<SurfaceOutput>() {
@Override
public void onSuccess(@Nullable SurfaceOutput output) {
@@ -339,6 +339,11 @@
abstract Size getSize();
/**
+ * The whether the stream should be mirrored.
+ */
+ abstract boolean getMirroring();
+
+ /**
* Creates an {@link OutConfig} instance from the input edge.
*
* <p>The result is an output edge with the input's transformation applied.
@@ -347,15 +352,17 @@
public static OutConfig of(@NonNull SurfaceEdge surface) {
return of(surface.getTargets(),
surface.getCropRect(),
- getRotatedSize(surface.getCropRect(), surface.getRotationDegrees()));
+ getRotatedSize(surface.getCropRect(), surface.getRotationDegrees()),
+ surface.getMirroring());
}
/**
* Creates an {@link OutConfig} instance with custom transformations.
*/
@NonNull
- public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size) {
- return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size);
+ public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size,
+ boolean mirroring) {
+ return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size, mirroring);
}
}
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 6dfefa5..e4cf2fe 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -144,10 +144,12 @@
assertThat(previewOutput.size).isEqualTo(rectToSize(expectedCropRect))
assertThat(previewOutput.cropRect).isEqualTo(expectedCropRect)
assertThat(previewOutput.rotationDegrees).isEqualTo(0)
+ assertThat(previewOutput.mirroring).isFalse()
val videoOutput = nodeOutput[videoOutConfig]!!
assertThat(videoOutput.size).isEqualTo(videoOutputSize)
assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(videoOutputSize))
assertThat(videoOutput.rotationDegrees).isEqualTo(0)
+ assertThat(videoOutput.mirroring).isTrue()
// Clean up.
nodeInput.surfaceEdge.close()
@@ -166,26 +168,6 @@
}
@Test
- fun transformInput_applyCropRotateAndMirroring_outputHasNoMirroring() {
- for (mirroring in arrayOf(false, true)) {
- // Arrange.
- createSurfaceProcessorNode()
- createInputEdge(mirroring = mirroring)
-
- // Act.
- val nodeOutput = node.transform(nodeInput)
-
- // Assert: the mirroring of output is always false.
- assertThat(nodeOutput[previewOutConfig]!!.mirroring).isFalse()
- assertThat(nodeOutput[videoOutConfig]!!.mirroring).isFalse()
-
- // Clean up.
- nodeInput.surfaceEdge.close()
- node.release()
- }
- }
-
- @Test
fun transformInput_applyCropRotateAndMirroring_initialTransformInfoIsPropagated() {
// Arrange.
createSurfaceProcessorNode()
@@ -206,6 +188,7 @@
assertThat(previewTransformInfo.cropRect).isEqualTo(Rect(0, 0, 400, 600))
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(0)
assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+ assertThat(previewSurfaceOutput.mirroring).isFalse()
val videoSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
@@ -215,6 +198,7 @@
assertThat(videoTransformInfo.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
assertThat(videoTransformInfo.rotationDegrees).isEqualTo(0)
assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+ assertThat(videoSurfaceOutput.mirroring).isTrue()
}
@Test
@@ -238,11 +222,13 @@
assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(90)
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(180)
assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+ assertThat(previewSurfaceOutput.mirroring).isFalse()
val videoSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(90)
assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+ assertThat(videoSurfaceOutput.mirroring).isTrue()
}
@Test
@@ -305,7 +291,8 @@
videoOutConfig = OutConfig.of(
VIDEO_CAPTURE,
VIDEO_CROP_RECT,
- videoOutputSize
+ videoOutputSize,
+ true
)
previewOutConfig = OutConfig.of(surface)
nodeInput = SurfaceProcessorNode.In.of(
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt
index 41f3813..c8ad021 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyStaggeredGridScrollingBenchmark.kt
@@ -83,6 +83,20 @@
}
@Test
+ fun scrollProgrammatically_newItemComposed_up() {
+ benchmarkRule.toggleStateBenchmark {
+ StaggeredGridRemeasureTestCase(
+ firstItemIndex = 100,
+ scrollUp = true,
+ addNewItemOnToggle = true,
+ content = testCase.content,
+ isVertical = testCase.isVertical,
+ usePointerInput = false
+ )
+ }
+ }
+
+ @Test
fun scrollViaPointerInput_noNewItems() {
benchmarkRule.toggleStateBenchmark {
StaggeredGridRemeasureTestCase(
@@ -95,6 +109,20 @@
}
@Test
+ fun scrollViaPointerInput_newItemComposed_up() {
+ benchmarkRule.toggleStateBenchmark {
+ StaggeredGridRemeasureTestCase(
+ firstItemIndex = 100,
+ scrollUp = true,
+ addNewItemOnToggle = true,
+ content = testCase.content,
+ isVertical = testCase.isVertical,
+ usePointerInput = true
+ )
+ }
+ }
+
+ @Test
fun scrollViaPointerInput_newItemComposed() {
benchmarkRule.toggleStateBenchmark {
StaggeredGridRemeasureTestCase(
@@ -203,6 +231,8 @@
@OptIn(ExperimentalFoundationApi::class)
class StaggeredGridRemeasureTestCase(
+ val firstItemIndex: Int = 0,
+ val scrollUp: Boolean = false,
val addNewItemOnToggle: Boolean,
val content: @Composable StaggeredGridRemeasureTestCase.(LazyStaggeredGridState) -> Unit,
val isVertical: Boolean,
@@ -216,6 +246,7 @@
private lateinit var motionEventHelper: MotionEventHelper
private var touchSlop: Float = 0f
private var scrollBy: Int = 0
+ private var targetItemOffset = 0
@Composable
fun FirstLargeItem() {
@@ -223,12 +254,23 @@
}
@Composable
+ fun RegularItem() {
+ Box(Modifier.requiredSize(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
+ }
+
+ @Composable
override fun Content() {
scrollBy = if (addNewItemOnToggle) {
with(LocalDensity.current) { 15.dp.roundToPx() }
} else {
5
+ } * if (scrollUp) -1 else 1
+ targetItemOffset = if (scrollUp) {
+ with(LocalDensity.current) { 20.dp.roundToPx() + scrollBy }
+ } else {
+ scrollBy
}
+
view = LocalView.current
if (!::motionEventHelper.isInitialized) motionEventHelper = MotionEventHelper(view)
touchSlop = LocalViewConfiguration.current.touchSlop
@@ -236,21 +278,16 @@
content(state)
}
- @Composable
- fun RegularItem() {
- Box(Modifier.requiredSize(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
- }
-
override fun beforeToggle() {
runBlocking {
- state.scrollToItem(0, 0)
+ state.scrollToItem(firstItemIndex, 0)
}
if (usePointerInput) {
val size = if (isVertical) view.measuredHeight else view.measuredWidth
motionEventHelper.sendEvent(MotionEvent.ACTION_DOWN, (size / 2f).toSingleAxisOffset())
motionEventHelper.sendEvent(MotionEvent.ACTION_MOVE, touchSlop.toSingleAxisOffset())
}
- assertEquals(0, state.firstVisibleItemIndex)
+ assertEquals(firstItemIndex, state.firstVisibleItemIndex)
assertEquals(0, state.firstVisibleItemScrollOffset)
}
@@ -266,8 +303,11 @@
}
override fun afterToggle() {
- assertEquals(0, state.firstVisibleItemIndex)
- assertEquals(scrollBy, state.firstVisibleItemScrollOffset)
+ assertEquals(
+ if (scrollUp) firstItemIndex - 2 else firstItemIndex,
+ state.firstVisibleItemIndex
+ )
+ assertEquals(targetItemOffset, state.firstVisibleItemScrollOffset)
if (usePointerInput) {
motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index 0e16498..64469cd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -16,10 +16,9 @@
package androidx.compose.foundation.text.selection
-import android.view.View
-import android.view.ViewGroup
-import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.TEST_FONT_FAMILY
@@ -30,6 +29,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.PointerEvent
@@ -43,27 +43,32 @@
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
+import androidx.compose.ui.test.TouchInjectionScope
import androidx.compose.ui.test.click
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.hasAnyChild
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.width
-import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -71,24 +76,22 @@
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
-import org.hamcrest.Description
+import java.util.concurrent.CountDownLatch
+import kotlin.math.max
+import kotlin.math.sign
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import kotlin.math.abs
-import kotlin.math.max
-import kotlin.math.roundToInt
-import org.junit.Ignore
@Suppress("DEPRECATION")
@LargeTest
@RunWith(AndroidJUnit4::class)
class SelectionContainerTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
-
- private lateinit var view: View
+ val rule = createComposeRule().also {
+ it.mainClock.autoAdvance = false
+ }
private val textContent = "Text Demo Text"
private val fontFamily = TEST_FONT_FAMILY
@@ -96,6 +99,9 @@
private val fontSize = 20.sp
private val log = PointerInputChangeLog()
+ private val tag1 = "tag1"
+ private val tag2 = "tag2"
+
private val hapticFeedback = mock<HapticFeedback>()
@Test
@@ -190,7 +196,7 @@
}
}
- @Ignore("b/230622412")
+ // @Ignore("b/230622412")
@Test
fun long_press_select_a_word_rtl_layout() {
with(rule.density) {
@@ -235,7 +241,230 @@
}
}
- private fun createSelectionContainer(isRtl: Boolean = false) {
+ @Test
+ fun selectionContinues_toBelowText() = with(rule.density) {
+ createSelectionContainer {
+ Column {
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag1),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+ )
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag2),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+ )
+ }
+ }
+
+ startSelection(tag1)
+ dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 3).bottomRight)
+
+ assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+ assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
+ }
+
+ @Test
+ fun selectionContinues_toAboveText() = with(rule.density) {
+ createSelectionContainer {
+ Column {
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag1),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+ )
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag2),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+ )
+ }
+ }
+
+ startSelection(tag2, offset = 6) // second word should be selected
+ dragHandleTo(Handle.SelectionStart, offset = characterBox(tag1, 5).bottomLeft)
+
+ assertAnchorInfo(selection.value?.start, offset = 5, selectableId = 1)
+ assertAnchorInfo(selection.value?.end, offset = 9, selectableId = 2)
+ }
+
+ @Test
+ fun selectionContinues_toNextText_skipsDisableSelection() = with(rule.density) {
+ createSelectionContainer {
+ Column {
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag1),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+ )
+ DisableSelection {
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag2),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
+ )
+ }
+ }
+ }
+
+ startSelection(tag1)
+ dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 3).bottomRight)
+
+ assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+ assertAnchorInfo(selection.value?.end, offset = textContent.length, selectableId = 1)
+ }
+
+ @Test
+ fun selectionHandle_remainsInComposition_whenTextIsOverflown_clipped_softwrapDisabled() {
+ createSelectionContainer {
+ Column {
+ BasicText(
+ AnnotatedString("$textContent ".repeat(100)),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag1),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ softWrap = false
+ )
+ DisableSelection {
+ BasicText(
+ textContent,
+ Modifier.fillMaxWidth(),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ softWrap = false
+ )
+ }
+ BasicText(
+ AnnotatedString("$textContent ".repeat(100)),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag2),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ softWrap = false
+ )
+ }
+ }
+
+ startSelection(tag1)
+ dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 3).bottomRight)
+
+ assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+ assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
+ }
+
+ @Ignore("b/262428141")
+ @Test
+ fun selectionHandle_remainsInComposition_whenTextIsOverflown_clipped_maxLines1() {
+ createSelectionContainer {
+ Column {
+ BasicText(
+ AnnotatedString("$textContent ".repeat(100)),
+ Modifier
+ .fillMaxWidth()
+ .testTag(tag1),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ maxLines = 1
+ )
+ DisableSelection {
+ BasicText(
+ textContent,
+ Modifier.fillMaxWidth().testTag(tag2),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ maxLines = 1
+ )
+ }
+ }
+ }
+
+ startSelection(tag1)
+ dragHandleTo(Handle.SelectionEnd, offset = characterBox(tag2, 4).center)
+
+ assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+ assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
+ }
+
+ private fun startSelection(
+ tag: String,
+ offset: Int = 0
+ ) {
+ val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+ val boundingBox = textLayoutResult.getBoundingBox(offset)
+ rule.onNodeWithTag(tag).performTouchInput {
+ longClick(boundingBox.center)
+ }
+ }
+
+ private fun characterBox(
+ tag: String,
+ offset: Int
+ ): Rect {
+ val nodePosition = rule.onNodeWithTag(tag).fetchSemanticsNode().positionInRoot
+ val textLayoutResult = rule.onNodeWithTag(tag).fetchTextLayoutResult()
+ return textLayoutResult.getBoundingBox(offset).translate(nodePosition)
+ }
+
+ private fun dragHandleTo(
+ handle: Handle,
+ offset: Offset
+ ) {
+ val position = rule.onNode(isSelectionHandle(handle))
+ .fetchSemanticsNode()
+ .config[SelectionHandleInfoKey]
+ .position
+ // selection handles are anchored at the bottom of a line.
+
+ rule.onNode(isSelectionHandle(handle)).performTouchInput {
+ val delta = offset - position
+ down(center)
+ movePastSlopBy(delta)
+ up()
+ }
+ }
+
+ /**
+ * Moves the first pointer by [delta] past the touch slop threshold on each axis.
+ * If [delta] is 0 on either axis it will stay 0.
+ */
+ private fun TouchInjectionScope.movePastSlopBy(delta: Offset) {
+ val slop = Offset(
+ x = viewConfiguration.touchSlop * delta.x.sign,
+ y = viewConfiguration.touchSlop * delta.y.sign
+ )
+ moveBy(delta + slop)
+ }
+
+ private fun assertAnchorInfo(
+ anchorInfo: Selection.AnchorInfo?,
+ resolvedTextDirection: ResolvedTextDirection = ResolvedTextDirection.Ltr,
+ offset: Int = 0,
+ selectableId: Long = 0
+ ) {
+ assertThat(anchorInfo).isEqualTo(
+ Selection.AnchorInfo(
+ resolvedTextDirection,
+ offset,
+ selectableId
+ )
+ )
+ }
+
+ private fun createSelectionContainer(
+ isRtl: Boolean = false,
+ content: (@Composable () -> Unit)? = null
+ ) {
val measureLatch = CountDownLatch(1)
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
@@ -244,7 +473,11 @@
LocalHapticFeedback provides hapticFeedback,
LocalLayoutDirection provides layoutDirection
) {
- TestParent(Modifier.testTag("selectionContainer").gestureSpy(log)) {
+ TestParent(
+ Modifier
+ .testTag("selectionContainer")
+ .gestureSpy(log)
+ ) {
SelectionContainer(
modifier = Modifier.onGloballyPositioned {
measureLatch.countDown()
@@ -254,7 +487,7 @@
selection.value = it
}
) {
- BasicText(
+ content?.invoke() ?: BasicText(
AnnotatedString(textContent),
Modifier.fillMaxSize(),
style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
@@ -268,42 +501,6 @@
}
}
}
- rule.activityRule.scenario.onActivity {
- view = it.findViewById<ViewGroup>(android.R.id.content)
- }
- }
-
- private fun matchesPosition(expectedX: Dp, expectedY: Dp): BoundedMatcher<View, View> {
- return object : BoundedMatcher<View, View>(View::class.java) {
- // (-1, -1) no position found
- var positionFound = IntOffset(-1, -1)
-
- override fun matchesSafely(item: View?): Boolean {
- with(rule.density) {
- val position = IntArray(2)
- item?.getLocationOnScreen(position)
- positionFound = IntOffset(position[0], position[1])
-
- val expectedPositionXInt = expectedX.value.roundToInt()
- val expectedPositionYInt = expectedY.value.roundToInt()
- val positionFoundXInt = positionFound.x.toDp().value.roundToInt()
- val positionFoundYInt = positionFound.y.toDp().value.roundToInt()
- return abs(expectedPositionXInt - positionFoundXInt) < 5 &&
- abs(expectedPositionYInt - positionFoundYInt) < 5
- }
- }
-
- override fun describeTo(description: Description?) {
- with(rule.density) {
- description?.appendText(
- "with expected position: " +
- "${expectedX.value}, ${expectedY.value} " +
- "but position found:" +
- "${positionFound.x.toDp().value}, ${positionFound.y.toDp().value}"
- )
- }
- }
- }
}
}
@@ -353,6 +550,19 @@
}
}
+/**
+ * Returns the text layout result caught by [SemanticsActions.GetTextLayoutResult]
+ * under this node. Throws an AssertionError if the node has not defined GetTextLayoutResult
+ * semantics action.
+ */
+fun SemanticsNodeInteraction.fetchTextLayoutResult(): TextLayoutResult {
+ val textLayoutResults = mutableListOf<TextLayoutResult>()
+ performSemanticsAction(SemanticsActions.GetTextLayoutResult) {
+ it(textLayoutResults)
+ }
+ return textLayoutResults.first()
+}
+
@Composable
fun TestParent(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index 319f135..3e2c1f1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -58,6 +58,7 @@
import com.google.common.truth.Truth.assertThat
import kotlin.math.ceil
import kotlin.math.floor
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -205,6 +206,50 @@
)
}
+ // TODO(b/265177763) Update this test to set MotionDurationScale to 0 when that's supported.
+ @Ignore("b/265177763")
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun cursorBlinkingAnimation_whenSystemDisablesAnimations() = with(rule.density) {
+ rule.setContent {
+ // The padding helps if the test is run accidentally in landscape. Landscape makes
+ // the cursor to be next to the navigation bar which affects the red color to be a bit
+ // different - possibly anti-aliasing.
+ Box(Modifier.padding(boxPadding)) {
+ BasicTextField(
+ value = "",
+ onValueChange = {},
+ textStyle = textStyle,
+ modifier = textFieldModifier,
+ cursorBrush = SolidColor(cursorColor),
+ onTextLayout = onTextLayout
+ )
+ }
+ }
+
+ focusAndWait()
+
+ // cursor visible first 500 ms
+ rule.mainClock.advanceTimeBy(100)
+ with(rule.density) {
+ rule.onNode(hasSetTextAction())
+ .captureToImage()
+ .assertCursor(2.dp, this, cursorRect)
+ }
+
+ // cursor invisible during next 500 ms
+ rule.mainClock.advanceTimeBy(700)
+ rule.onNode(hasSetTextAction())
+ .captureToImage()
+ .assertShape(
+ density = rule.density,
+ shape = RectangleShape,
+ shapeColor = Color.White,
+ backgroundColor = Color.White,
+ shapeOverlapPixelCount = 0.0f
+ )
+ }
+
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun cursorUnsetColor_noCursor() = with(rule.density) {
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 b6e53ad..222e742 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
@@ -115,9 +115,7 @@
initialVelocity: Float,
onSettlingDistanceUpdated: (Float) -> Unit
): Float {
- val (remainingOffset, remainingState) = withContext(motionScaleDuration) {
- fling(initialVelocity, onSettlingDistanceUpdated)
- }
+ val (remainingOffset, remainingState) = fling(initialVelocity, onSettlingDistanceUpdated)
debugLog { "Post Settling Offset=$remainingOffset" }
// No remaining offset means we've used everything, no need to propagate velocity. Otherwise
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 1c365bf..28210ac 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
@@ -184,7 +184,7 @@
}
inline val SpanRange.isFullSpan: Boolean
- get() = end - start != 1
+ get() = size != 1
inline val SpanRange.laneInfo: Int
get() = if (isFullSpan) FullSpan else start
@@ -234,7 +234,7 @@
// this will contain all the MeasuredItems representing the visible items
val measuredItems = Array(laneCount) {
- ArrayDeque<LazyStaggeredGridMeasuredItem>()
+ ArrayDeque<LazyStaggeredGridMeasuredItem>(16)
}
// include the start padding so we compose items in the padding area. before starting
@@ -315,26 +315,31 @@
fun misalignedStart(referenceLane: Int): Boolean {
// If we scrolled past the first item in the lane, we have a point of reference
// to re-align items.
- val laneRange = firstItemIndices.indices
// Case 1: Each lane has laid out all items, but offsets do no match
- val misalignedOffsets = laneRange.any { lane ->
- findPreviousItemIndex(firstItemIndices[lane], lane) == Unset &&
- firstItemOffsets[lane] != firstItemOffsets[referenceLane]
+ for (lane in firstItemIndices.indices) {
+ val misalignedOffsets =
+ findPreviousItemIndex(firstItemIndices[lane], lane) == Unset &&
+ firstItemOffsets[lane] != firstItemOffsets[referenceLane]
+
+ if (misalignedOffsets) {
+ return true
+ }
}
// Case 2: Some lanes are still missing items, and there's no space left to place them
- val moreItemsInOtherLanes = laneRange.any { lane ->
- findPreviousItemIndex(firstItemIndices[lane], lane) != Unset &&
- firstItemOffsets[lane] >= firstItemOffsets[referenceLane]
+ for (lane in firstItemIndices.indices) {
+ val moreItemsInOtherLanes =
+ findPreviousItemIndex(firstItemIndices[lane], lane) != Unset &&
+ firstItemOffsets[lane] >= firstItemOffsets[referenceLane]
+
+ if (moreItemsInOtherLanes) {
+ return true
+ }
}
// Case 3: the first item is in the wrong lane (it should always be in
// the first one)
val firstItemLane = laneInfo.getLane(0)
- val firstItemInWrongLane =
- firstItemLane != 0 && firstItemLane != Unset && firstItemLane != FullSpan
- // If items are not aligned, reset all measurement data we gathered before and
- // proceed with initial measure
- return misalignedOffsets || moreItemsInOtherLanes || firstItemInWrongLane
+ return firstItemLane != 0 && firstItemLane != Unset && firstItemLane != FullSpan
}
// define min offset (currently includes beforeContentPadding)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index c9ff972..c9af356 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -18,7 +18,7 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.core.spring
@@ -66,7 +66,11 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastSumBy
+import kotlin.math.absoluteValue
+import kotlin.math.ceil
+import kotlin.math.floor
import kotlin.math.roundToInt
+import kotlin.math.sign
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@@ -458,7 +462,10 @@
fun flingBehavior(
state: PagerState,
pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
- lowVelocityAnimationSpec: AnimationSpec<Float> = tween(easing = LinearOutSlowInEasing),
+ lowVelocityAnimationSpec: AnimationSpec<Float> = tween(
+ easing = LinearEasing,
+ durationMillis = LowVelocityAnimationDefaultDuration
+ ),
highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
): SnapFlingBehavior {
@@ -616,18 +623,30 @@
}
override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
- val effectivePageSize = pagerState.pageSize + pagerState.pageSpacing
- val initialOffset = pagerState.currentPageOffsetFraction * effectivePageSize
- val animationOffset =
+ val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
+ val scrollOffset = pagerState.currentPageOffsetFraction * effectivePageSizePx
+ val animationOffsetPx =
decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
-
val startPage = pagerState.currentPage
- val startPageOffset = startPage * effectivePageSize
- val targetOffset =
- (startPageOffset + initialOffset + animationOffset) / effectivePageSize
+ debugLog {
+ "Initial Offset=$scrollOffset " +
+ "\nAnimation Offset=$animationOffsetPx " +
+ "\nFling Start Page=$startPage " +
+ "\nEffective Page Size=$effectivePageSizePx"
+ }
- val targetPage = targetOffset.toInt()
+ val targetOffsetPx = startPage * effectivePageSizePx + animationOffsetPx
+
+ val targetPageValue = targetOffsetPx / effectivePageSizePx
+ val targetPage = if (initialVelocity > 0) {
+ ceil(targetPageValue)
+ } else {
+ floor(targetPageValue)
+ }.toInt().coerceIn(0, pagerState.pageCount)
+
+ debugLog { "Fling Target Page=$targetPage" }
+
val correctedTargetPage = pagerSnapDistance.calculateTargetPage(
startPage,
targetPage,
@@ -636,9 +655,20 @@
pagerState.pageSpacing
).coerceIn(0, pagerState.pageCount)
- val finalOffset = (correctedTargetPage - startPage) * effectivePageSize
+ debugLog { "Fling Corrected Target Page=$correctedTargetPage" }
- return (finalOffset - initialOffset)
+ val proposedFlingOffset = (correctedTargetPage - startPage) * effectivePageSizePx
+
+ val flingApproachOffsetPx =
+ (proposedFlingOffset.absoluteValue - scrollOffset.absoluteValue).coerceAtLeast(0f)
+
+ return if (flingApproachOffsetPx == 0f) {
+ flingApproachOffsetPx
+ } else {
+ flingApproachOffsetPx * initialVelocity.sign
+ }.also {
+ debugLog { "Fling Approach Offset=$it" }
+ }
}
}
}
@@ -732,4 +762,13 @@
pageRight { performForwardPaging() }
}
})
-}
\ No newline at end of file
+}
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("Pager: ${generateMsg()}")
+ }
+}
+
+private const val LowVelocityAnimationDefaultDuration = 500
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 9e3faa2..7680e6a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -207,8 +207,8 @@
0f
}
val pageDisplacement =
- (offsetFromFling + offsetFromScroll).roundToInt() / pageAvailableSpace
- (currentPage + pageDisplacement).coerceInPageRange()
+ (offsetFromFling + offsetFromScroll) / pageAvailableSpace
+ (currentPage + pageDisplacement.roundToInt()).coerceInPageRange()
}
}
@@ -298,6 +298,11 @@
} else {
page + visiblePages.size.coerceAtMost(currentPosition)
}
+
+ debugLog {
+ "animateScrollToPage with pre-jump to position=$preJumpPosition"
+ }
+
// Pre-jump to 1 viewport away from destination item, if possible
requireNotNull(lazyListState).scrollToItem(preJumpPosition)
currentPosition = preJumpPosition
@@ -309,6 +314,10 @@
distanceToSnapPosition + pageOffsetFraction * pageAvailableSpace
val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
+
+ debugLog {
+ "animateScrollToPage $displacement pixels"
+ }
requireNotNull(lazyListState).animateScrollBy(displacement, animationSpec)
animationTargetPage = -1
}
@@ -427,4 +436,11 @@
private val EmptyInteractionSources = object : InteractionSource {
override val interactions: Flow<Interaction>
get() = emptyFlow()
+}
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("PagerState: ${generateMsg()}")
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index bf87a63..69c0ab3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -23,6 +23,7 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
@@ -33,6 +34,7 @@
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.withContext
@Suppress("ModifierInspectorInfo")
internal fun Modifier.cursor(
@@ -46,10 +48,13 @@
val isBrushSpecified = !(cursorBrush is SolidColor && cursorBrush.value.isUnspecified)
if (state.hasFocus && value.selection.collapsed && isBrushSpecified) {
LaunchedEffect(value.annotatedString, value.selection) {
- // ensure that the value is always 1f _this_ frame by calling snapTo
- cursorAlpha.snapTo(1f)
- // then start the cursor blinking on animation clock (500ms on to start)
- cursorAlpha.animateTo(0f, cursorAnimationSpec)
+ // Animate the cursor even when animations are disabled by the system.
+ withContext(FixedMotionDurationScale) {
+ // ensure that the value is always 1f _this_ frame by calling snapTo
+ cursorAlpha.snapTo(1f)
+ // then start the cursor blinking on animation clock (500ms on to start)
+ cursorAlpha.animateTo(0f, cursorAnimationSpec)
+ }
}
drawWithContent {
this.drawContent()
@@ -88,3 +93,8 @@
)
internal val DefaultCursorThickness = 2.dp
+
+private object FixedMotionDurationScale : MotionDurationScale {
+ override val scaleFactor: Float
+ get() = 1f
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 87053fb..e249b73 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -334,10 +334,19 @@
)
val visibleBounds = containerCoordinates.visibleBounds()
- this.startHandlePosition =
- if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
- this.endHandlePosition =
- if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
+
+ // set the new handle position only if the handle is in visible bounds or
+ // the handle is still dragging. If handle goes out of visible bounds during drag, handle
+ // popup is also removed from composition, halting the drag gesture. This affects multiple
+ // text selection when selected text is configured with maxLines=1 and overflow=clip.
+ this.startHandlePosition = startHandlePosition.takeIf {
+ visibleBounds.containsInclusive(startHandlePosition) ||
+ draggingHandle == Handle.SelectionStart
+ }
+ this.endHandlePosition = endHandlePosition.takeIf {
+ visibleBounds.containsInclusive(endHandlePosition) ||
+ draggingHandle == Handle.SelectionEnd
+ }
}
/**
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
index 9c4339a..23b8cb7 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
@@ -26,8 +26,8 @@
import com.intellij.psi.util.ClassUtil
import kotlinx.metadata.KmDeclarationContainer
import kotlinx.metadata.KmFunction
-import kotlinx.metadata.jvm.KotlinClassHeader
import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.Metadata
import kotlinx.metadata.jvm.signature
/**
@@ -48,12 +48,12 @@
* represents a synthetic class.
*/
private fun PsiClass.getKmDeclarationContainer(): KmDeclarationContainer? {
- val classKotlinMetadataAnnotation = annotations.find {
+ val classKotlinMetadataPsiAnnotation = annotations.find {
// hasQualifiedName() not available on the min version of Lint we compile against
it.qualifiedName == KotlinMetadataFqn
} ?: return null
- val metadata = KotlinClassMetadata.read(classKotlinMetadataAnnotation.toHeader())
+ val metadata = KotlinClassMetadata.read(classKotlinMetadataPsiAnnotation.toMetadataAnnotation())
?: return null
return when (metadata) {
@@ -67,12 +67,9 @@
}
/**
- * Returns a [KotlinClassHeader] by parsing the attributes of this @kotlin.Metadata annotation.
- *
- * See: https://github.com/udalov/kotlinx-metadata-examples/blob/master/src/main/java
- * /examples/FindKotlinGeneratedMethods.java
+ * Returns a [Metadata] by parsing the attributes of this @kotlin.Metadata PSI annotation.
*/
-private fun PsiAnnotation.toHeader(): KotlinClassHeader {
+private fun PsiAnnotation.toMetadataAnnotation(): Metadata {
val attributes = attributes.associate { it.attributeName to it.attributeValue }
fun JvmAnnotationAttributeValue.parseString(): String =
@@ -99,7 +96,7 @@
val packageName = attributes["pn"]?.parseString()
val extraInt = attributes["xi"]?.parseInt()
- return KotlinClassHeader(
+ return Metadata(
kind,
metadataVersion,
data1,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
index 73e69950..6ea5bee 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalMaterialApi::class)
+
package androidx.compose.material.swipeable
import androidx.compose.foundation.background
@@ -35,7 +37,6 @@
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
-@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun SwipeableBox(
swipeableState: SwipeableV2State<TestState>,
@@ -47,7 +48,28 @@
),
enabled: Boolean = true,
reverseDirection: Boolean = false,
- calculateAnchor: (state: TestState, layoutSize: IntSize) -> Float? = { state, layoutSize ->
+ anchors: Map<TestState, Float>
+) = SwipeableBox(
+ swipeableState = swipeableState,
+ orientation = orientation,
+ possibleStates = possibleStates,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ calculateAnchor = { anchor, _ -> anchors[anchor] }
+)
+
+@Composable
+internal fun SwipeableBox(
+ swipeableState: SwipeableV2State<TestState>,
+ orientation: Orientation = Orientation.Horizontal,
+ possibleStates: Set<TestState> = setOf(
+ TestState.A,
+ TestState.B,
+ TestState.C
+ ),
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false,
+ calculateAnchor: (anchor: TestState, layoutSize: IntSize) -> Float? = { state, layoutSize ->
val size = (
if (orientation == Orientation.Horizontal) layoutSize.width else layoutSize.height
).toFloat()
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
index 3615573..7840ba1 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
@@ -29,10 +29,11 @@
import androidx.compose.material.swipeable.TestState.A
import androidx.compose.material.swipeable.TestState.B
import androidx.compose.material.swipeable.TestState.C
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
@@ -64,104 +65,174 @@
val rule = createComposeRule()
@Test
- fun swipeable_swipe_horizontal() = directionalSwipeTest(
- SwipeableTestState(initialState = A),
- orientation = Orientation.Horizontal,
- verify = verifyOffsetMatchesAnchor()
- )
+ fun swipeable_swipe_horizontal() {
+ val state = SwipeableTestState(initialState = A)
+ val anchors = mapOf(
+ A to 0f,
+ B to 250f,
+ C to 500f
+ )
- @Test
- fun swipeable_swipe_vertical() = directionalSwipeTest(
- SwipeableTestState(initialState = A),
- orientation = Orientation.Vertical,
- verify = verifyOffsetMatchesAnchor()
- )
-
- @Test
- fun swipeable_swipe_disabled_horizontal() = directionalSwipeTest(
- SwipeableTestState(initialState = A),
- orientation = Orientation.Horizontal,
- enabled = false,
- verify = verifyOffset0(),
- )
-
- @Test
- fun swipeable_swipe_disabled_vertical(): Unit = directionalSwipeTest(
- SwipeableTestState(initialState = A),
- orientation = Orientation.Vertical,
- enabled = false,
- verify = verifyOffset0(),
- )
-
- @Test
- fun swipeable_swipe_reverse_direction_horizontal() = directionalSwipeTest(
- SwipeableTestState(initialState = A),
- orientation = Orientation.Horizontal,
- verify = verifyOffsetMatchesAnchor()
- )
-
- private fun verifyOffset0() = { state: SwipeableV2State<TestState>, _: TestState ->
- assertThat(state.offset).isEqualTo(0f)
- }
-
- private fun verifyOffsetMatchesAnchor() =
- { state: SwipeableV2State<TestState>, target: TestState ->
- val swipeableSizePx = with(rule.density) { swipeableSize.roundToPx() }
- val targetOffset = when (target) {
- A -> 0f
- B -> swipeableSizePx / 2
- C -> swipeableSizePx
- }
- assertThat(state.offset).isEqualTo(targetOffset)
- }
-
- private fun directionalSwipeTest(
- state: SwipeableV2State<TestState>,
- orientation: Orientation,
- enabled: Boolean = true,
- reverseDirection: Boolean = false,
- swipeForward: TouchInjectionScope.(end: Float) -> Unit = {
- if (orientation == Orientation.Horizontal) swipeRight(endX = it)
- else swipeDown(endY = it)
- },
- swipeBackward: TouchInjectionScope.(end: Float) -> Unit = {
- if (orientation == Orientation.Horizontal) swipeLeft(endX = it) else swipeUp(endY = it)
- },
- verify: (state: SwipeableV2State<TestState>, target: TestState) -> Unit,
- ) {
rule.setContent {
- SwipeableBox(state, orientation, enabled = enabled, reverseDirection = reverseDirection)
+ CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+ WithTouchSlop(0f) {
+ SwipeableBox(
+ swipeableState = state,
+ orientation = Orientation.Horizontal,
+ anchors = anchors
+ )
+ }
+ }
}
- rule.onNodeWithTag(swipeableTestTag)
- .performTouchInput { swipeForward(endEdge(orientation) / 2) }
- rule.waitForIdle()
- verify(state, B)
+ assertThat(state.currentValue).isEqualTo(A)
rule.onNodeWithTag(swipeableTestTag)
- .performTouchInput { swipeForward(endEdge(orientation)) }
+ .performTouchInput { swipeRight(endX = right / 2) }
rule.waitForIdle()
- verify(state, C)
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.offset).isEqualTo(anchors.getValue(B))
rule.onNodeWithTag(swipeableTestTag)
- .performTouchInput { swipeBackward(endEdge(orientation) / 2) }
+ .performTouchInput { swipeRight(startX = right / 2, endX = right) }
rule.waitForIdle()
- verify(state, B)
+ assertThat(state.currentValue).isEqualTo(C)
+ assertThat(state.offset).isEqualTo(anchors.getValue(C))
rule.onNodeWithTag(swipeableTestTag)
- .performTouchInput { swipeBackward(startEdge(orientation)) }
+ .performTouchInput { swipeLeft(endX = right / 2) }
rule.waitForIdle()
- verify(state, A)
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.offset).isEqualTo(anchors.getValue(B))
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeLeft(startX = right / 2) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.offset).isEqualTo(anchors.getValue(A))
}
@Test
- fun swipeable_positionalThresholds_fractional_targetState() = fractionalThresholdsTest(0.5f)
+ fun swipeable_swipe_vertical() {
+ val state = SwipeableTestState(initialState = A)
+ val anchors = mapOf(
+ A to 0f,
+ B to 250f,
+ C to 500f
+ )
+
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+ WithTouchSlop(0f) {
+ SwipeableBox(
+ swipeableState = state,
+ orientation = Orientation.Vertical,
+ anchors = anchors
+ )
+ }
+ }
+ }
+
+ assertThat(state.currentValue).isEqualTo(A)
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeDown(startY = top, endY = bottom / 2) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.offset).isEqualTo(anchors.getValue(B))
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeDown(startY = bottom / 2, endY = bottom) }
+ rule.waitForIdle()
+ assertThat(state.currentValue).isEqualTo(C)
+ assertThat(state.offset).isEqualTo(anchors.getValue(C))
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeUp(startY = bottom, endY = bottom / 2) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.offset).isEqualTo(anchors.getValue(B))
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeUp(startY = bottom / 2, endY = top) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.offset).isEqualTo(anchors.getValue(A))
+ }
@Test
- fun swipeable_positionalThresholds_fractional_negativeThreshold_targetState() =
- fractionalThresholdsTest(-0.5f)
+ fun swipeable_swipe_disabled_horizontal() {
+ val state = SwipeableTestState(initialState = A)
+ val anchors = mapOf(
+ A to 0f,
+ B to 250f,
+ C to 500f
+ )
- private fun fractionalThresholdsTest(positionalThreshold: Float) {
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+ WithTouchSlop(0f) {
+ SwipeableBox(
+ swipeableState = state,
+ orientation = Orientation.Horizontal,
+ anchors = anchors,
+ enabled = false
+ )
+ }
+ }
+ }
+
+ assertThat(state.currentValue).isEqualTo(A)
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeRight(startX = left, endX = right) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.offset).isZero()
+ }
+
+ @Test
+ fun swipeable_swipe_disabled_vertical() {
+ val state = SwipeableTestState(initialState = A)
+ val anchors = mapOf(
+ A to 0f,
+ B to 250f,
+ C to 500f
+ )
+
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+ WithTouchSlop(0f) {
+ SwipeableBox(
+ swipeableState = state,
+ orientation = Orientation.Vertical,
+ anchors = anchors,
+ enabled = false
+ )
+ }
+ }
+ }
+
+ assertThat(state.currentValue).isEqualTo(A)
+
+ rule.onNodeWithTag(swipeableTestTag)
+ .performTouchInput { swipeDown(startY = top, endY = bottom) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.offset).isZero()
+ }
+
+ @Test
+ fun swipeable_positionalThresholds_fractional_targetState() {
+ val positionalThreshold = 0.5f
val absThreshold = abs(positionalThreshold)
val state = SwipeableTestState(
initialState = A,
@@ -185,6 +256,7 @@
assertThat(state.targetValue).isEqualTo(B)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
assertThat(state.currentValue).isEqualTo(B)
assertThat(state.targetValue).isEqualTo(B)
@@ -202,19 +274,65 @@
assertThat(state.targetValue).isEqualTo(A)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
assertThat(state.currentValue).isEqualTo(A)
assertThat(state.targetValue).isEqualTo(A)
}
@Test
- fun swipeable_positionalThresholds_fixed_targetState() = fixedThresholdsTest(56.dp)
+ fun swipeable_positionalThresholds_fractional_negativeThreshold_targetState() {
+ val positionalThreshold = -0.5f
+ val absThreshold = abs(positionalThreshold)
+ val state = SwipeableTestState(
+ initialState = A,
+ positionalThreshold = fractionalPositionalThreshold(positionalThreshold)
+ )
+ rule.setContent { SwipeableBox(state) }
+
+ val positionOfA = state.anchors.getValue(A)
+ val positionOfB = state.anchors.getValue(B)
+ val distance = abs(positionOfA - positionOfB)
+ state.dispatchRawDelta(positionOfA + distance * (absThreshold * 0.9f))
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ state.dispatchRawDelta(distance * 0.2f)
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ state.dispatchRawDelta(-distance * (absThreshold * 0.9f))
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ state.dispatchRawDelta(-distance * 0.2f)
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+ }
@Test
- fun swipeable_positionalThresholds_fixed_negativeThreshold_targetState() =
- fixedThresholdsTest((-56).dp)
-
- private fun fixedThresholdsTest(positionalThreshold: Dp) {
+ fun swipeable_positionalThresholds_fixed_targetState() {
+ val positionalThreshold = 56.dp
val absThreshold = with(rule.density) { abs(positionalThreshold.toPx()) }
val state = SwipeableTestState(
initialState = A,
@@ -239,6 +357,7 @@
assertThat(state.targetValue).isEqualTo(B)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
assertThat(state.currentValue).isEqualTo(B)
assertThat(state.targetValue).isEqualTo(B)
@@ -258,6 +377,60 @@
assertThat(state.targetValue).isEqualTo(A)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+ }
+
+ @Test
+ fun swipeable_positionalThresholds_fixed_negativeThreshold_targetState() {
+ val positionalThreshold = (-56).dp
+ val absThreshold = with(rule.density) { abs(positionalThreshold.toPx()) }
+ val state = SwipeableTestState(
+ initialState = A,
+ positionalThreshold = fixedPositionalThreshold(positionalThreshold)
+ )
+ rule.setContent { SwipeableBox(state) }
+
+ val initialOffset = state.requireOffset()
+
+ // Swipe towards B, close before threshold
+ state.dispatchRawDelta(initialOffset + (absThreshold * 0.9f))
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ // Swipe towards B, close after threshold
+ state.dispatchRawDelta(absThreshold * 0.2f)
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ // Swipe towards A, close before threshold
+ state.dispatchRawDelta(-(absThreshold * 0.9f))
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ // Swipe towards A, close after threshold
+ state.dispatchRawDelta(-(absThreshold * 0.2f))
+ rule.waitForIdle()
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+ rule.waitForIdle()
assertThat(state.currentValue).isEqualTo(A)
assertThat(state.targetValue).isEqualTo(A)
@@ -268,19 +441,18 @@
runBlocking(AutoTestFrameClock()) {
val velocity = 100.dp
val velocityPx = with(rule.density) { velocity.toPx() }
- val state = with(rule) {
- SwipeableTestState(
- initialState = A,
- anchors = mapOf(
- A to 0f,
- B to 100f,
- C to 200f
- ),
- velocityThreshold = velocity / 2
- )
- }
+ val state = SwipeableTestState(
+ initialState = A,
+ anchors = mapOf(
+ A to 0f,
+ B to 100f,
+ C to 200f
+ ),
+ velocityThreshold = velocity / 2
+ )
state.dispatchRawDelta(60f)
state.settle(velocityPx)
+ rule.waitForIdle()
assertThat(state.currentValue).isEqualTo(B)
}
@@ -451,10 +623,9 @@
if (anchors != null) updateAnchors(anchors)
this.density = density
}
+}
- private fun TouchInjectionScope.endEdge(orientation: Orientation) =
- if (orientation == Orientation.Horizontal) right else bottom
-
- private fun TouchInjectionScope.startEdge(orientation: Orientation) =
- if (orientation == Orientation.Horizontal) left else top
-}
\ No newline at end of file
+private val NoOpDensity = object : Density {
+ override val density = 1f
+ override val fontScale = 1f
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
index 84336f2..e797721 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
@@ -10,7 +10,7 @@
In this page, you'll find documentation for types, properties, and functions available in the `androidx.compose.material3` package.
-For more information, check out the <a href="https://developer.android.com/jetpack/compose/themes/material#material3" class="external" target="_blank">Material Design 3 and Material You</a> section in the Material Theming in Compose guide.
+For more information, check out <a href="https://developer.android.com/jetpack/compose/designsystems/material3" class="external" target="_blank">Material Design 3 in Compose</a>.
## Overview
diff --git a/docs/api_guidelines/compat.md b/docs/api_guidelines/compat.md
index 3b2a251..cbc7bc8 100644
--- a/docs/api_guidelines/compat.md
+++ b/docs/api_guidelines/compat.md
@@ -245,22 +245,52 @@
}
```
-### Inter-process communication {#inter-process-communication}
+### Inter-process communication {#ipc}
Protocols and data structures used for IPC must support interoperability between
-different versions of libraries and should be treated similarly to public API.
+different versions of libraries and should be treated similarly to public API;
+however, AndroidX does not currently implement compatibility tracking for IPC.
-#### Data structures
+We recommend the following, in order of preference:
-**Do not** use `Parcelable` for any class that may be used for IPC or otherwise
-exposed as public API. The wire format used by `Parcelable` does not provide any
-compatibility guarantees and will result in crashes if fields are added or
-removed between library versions.
+1. Stable AIDL if (1) your project lives partially in the Android platform and
+ has access to Stable AIDL build rules and (2) you need to support Android's
+ `Parcelable` data types. The AndroidX workflow **does not** provide Stable
+ AIDL compilation or compatibility checks, so these would need to happen in
+ the platform build and the resulting `.java` files would need to be copied
+ out.
+2. Protobuf if (1) your project needs to persist data to disk or (2) you need
+ interoperability with systems already using Protobuf. Similar to Stable
+ AIDL, the AndroidX workflow **does not** provide built-in support Protobuf
+ compilation or compatibility checks. It is possible to use a Proto plug-in,
+ but you will be responsible for bundling the runtime and maintaining
+ compatibility on your own.
+3. Bundle if you have a very simple data model that is unlikely to change in
+ the future. Bundle has the weakest type safety and compatibility guarantees
+ of any recommendation, and it has many caveats that make it a poor choice.
+4. `VersionedParcelable` if your project is already using Versioned Parcelable
+ and is aware of its compatibility constraints.
**Do not** design your own serialization mechanism or wire format for disk
storage or inter-process communication. Preserving and verifying compatibility
is difficult and error-prone.
+In all cases, **do not** expose your serialization mechanism in your API
+surface. Neither Stable AIDL nor Protobuf generate stable language APIs.
+
+#### Parcelable {#ipc-parcelable}
+
+**Do not** implement `Parcelable` for any class that may be used for IPC or
+otherwise exposed as public API. By default, `Parcelable` does not provide any
+compatibility guarantees and will result in crashes if fields are added or
+removed between library versions. If you are using Stable AIDL, you *may* use
+AIDL-defined parcelables for IPC but not public API.
+
+NOTE As of 2022/12/16, we are working on experimental support for compiling and
+tracking Stable AIDL definitions within the AndroidX workflow.
+
+#### Protobuf {#ipc-protobuf}
+
Developers **should** use protocol buffers for most cases. See
[Protobuf](#dependencies-protobuf) for more information on using protocol
buffers in your library. **Do** use protocol buffers if your data structure is
@@ -268,6 +298,14 @@
`Binder`s, or other platform-defined `Parcelable` data structures, they will
need to be stored alongside the protobuf bytes in a `Bundle`.
+NOTE We are currently investigating the suitability of Square's
+[`wire` library](https://github.com/square/wire) for handling protocol buffers
+in Android libraries. If adopted, it will replace `proto` library dependencies.
+Libraries that expose their serialization mechanism in their API surface *will
+not be able to migrate*.
+
+#### Bundle {#ipc-bundle}
+
Developers **may** use `Bundle` in simple cases that require sending `Binder`s,
`FileDescriptor`s, or platform `Parcelable`s across IPC
([example](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java;l=820)).
@@ -290,19 +328,7 @@
are responsible for providing their own system for guaranteeing wire format
compatibility between versions.
-Developers **may** use `VersionedParcelable` in cases where they are already
-using the library and understand its limitations.
-
-In all cases, **do not** expose your serialization mechanism in your API
-surface.
-
-NOTE We are currently investigating the suitability of Square's
-[`wire` library](https://github.com/square/wire) for handling protocol buffers
-in Android libraries. If adopted, it will replace `proto` library dependencies.
-Libraries that expose their serialization mechanism in their API surface *will
-not be able to migrate*.
-
-#### Communication protocols
+#### Communication protocols {#ipc-protocol}
Any communication prototcol, handshake, etc. must maintain compatibility
consistent with SemVer guidelines. Consider how your protocol will handle
diff --git a/docs/api_guidelines/dependencies.md b/docs/api_guidelines/dependencies.md
index bd0bfe9..83bdad6 100644
--- a/docs/api_guidelines/dependencies.md
+++ b/docs/api_guidelines/dependencies.md
@@ -95,8 +95,9 @@
more individual owners (e.g. NOT a group alias)
* Library **must** be approved by legal
-Please see Jetpack's [open-source policy page](open_source.md) for more details
-on using third-party libraries.
+Please see Jetpack's
+[open-source policy page](/company/teams/androidx/open_source.md) for more
+details on using third-party libraries.
### Types of dependencies {#dependencies-types}
@@ -145,8 +146,16 @@
between artifacts
([example](https://android-review.googlesource.com/c/platform/frameworks/support/+/2086029)).
-`core/core-ktx/build.gradle`: `dependencies { // Atomic group constraints {
-implementation(project(":core:core")) } }`
+`core/core-ktx/build.gradle`:
+
+```
+dependencies {
+ // Atomic group
+ constraints {
+ implementation(project(":core:core"))
+ }
+}
+```
In *extremely* rare cases, libraries may need to define a constraint on a
project that is not in its `studiow` project set, ex. a constraint between the
@@ -155,10 +164,16 @@
([example](https://android-review.googlesource.com/c/platform/frameworks/support/+/2160202))
to indicate the tip-of-tree version.
-`paging/paging-common.build.gradle`: `dependencies { // Atomic Group constraints
-{
-implementation("androidx.paging:paging-compose:${LibraryVersions.PAGING_COMPOSE}")
-}`
+`paging/paging-common/build.gradle`:
+
+```
+dependencies {
+ // Atomic group
+ constraints {
+ implementation("androidx.paging:paging-compose:${LibraryVersions.PAGING_COMPOSE}")
+ }
+}
+```
### System health {#dependencies-health}
@@ -184,16 +199,24 @@
Kotlin's coroutine library adds around 100kB post-shrinking. New libraries that
are written in Kotlin should prefer coroutines over `ListenableFuture`, but
existing libraries must consider the size impact on their clients. See
-[Asynchronous work with return values](#async-return) for more details on using
-Kotlin coroutines in Jetpack libraries.
+[Asynchronous work with return values](/company/teams/androidx/api_guidelines#async-return)
+for more details on using Kotlin coroutines in Jetpack libraries.
#### Guava {#dependencies-guava}
-The full Guava library is very large and *must not* be used. Libraries that
-would like to depend on Guava's `ListenableFuture` may instead depend on the
-standalone `com.google.guava:listenablefuture` artifact. See
-[Asynchronous work with return values](#async-return) for more details on using
-`ListenableFuture` in Jetpack libraries.
+The full Guava library is very large and should only be used in cases where
+there is a reasonable assumption that clients already depend on full Guava.
+
+For example, consider a library `androidx.foo:foo` implemented using Kotlin
+`suspend fun`s and an optional `androidx.foo:foo-guava` library that provides
+`ListenableFuture` interop wrappers with a direct dependency on
+`kotlinx.coroutines:kotlinx-coroutines-guava` and a transitive dependency on
+Guava.
+
+Libraries that only need `ListenableFuture` may instead depend on the standalone
+`com.google.guava:listenablefuture` artifact. See
+[Asynchronous work with return values](/company/teams/androidx/api_guidelines#async-return)
+for more details on using `ListenableFuture` in Jetpack libraries.
#### Protobuf {#dependencies-protobuf}
diff --git a/docs/api_guidelines/deprecation.md b/docs/api_guidelines/deprecation.md
index 0ca80db..4539ca6 100644
--- a/docs/api_guidelines/deprecation.md
+++ b/docs/api_guidelines/deprecation.md
@@ -27,7 +27,8 @@
APIs that are added during a pre-release cycle and marked as `@Deprecated`
within the same cycle, e.g. added in `alpha01` and deprecated in `alpha06`,
-[must be removed](versioning.md#beta-checklist) before moving to `beta01`.
+[must be removed](/company/teams/androidx/versioning.md#beta-checklist) before
+moving to `beta01`.
### Soft removal (`@removed` or `DeprecationLevel.HIDDEN`)
diff --git a/docs/api_guidelines/misc.md b/docs/api_guidelines/misc.md
index 7ab2bd8..34bc0ee 100644
--- a/docs/api_guidelines/misc.md
+++ b/docs/api_guidelines/misc.md
@@ -435,6 +435,17 @@
## Kotlin-specific guidelines {#kotlin}
+Generally speaking, Kotlin code should follow the compatibility guidelines
+outlined at:
+
+- The official Android Developers
+ [Kotlin-Java interop guide](https://developer.android.com/kotlin/interop)
+- Android API guidelines for
+ [Kotlin-Java interop](https://android.googlesource.com/platform/developers/docs/+/refs/heads/master/api-guidelines/index.md#kotin-interop)
+- Android API guidelines for
+ [asynchronous and non-blocking APIs](https://android.googlesource.com/platform/developers/docs/+/refs/heads/master/api-guidelines/async.md)
+- Library-specific guidance outlined below
+
### Nullability
#### Annotations on new Java APIs
diff --git a/docs/api_guidelines/modules.md b/docs/api_guidelines/modules.md
index 08e3433..b7bf26f 100644
--- a/docs/api_guidelines/modules.md
+++ b/docs/api_guidelines/modules.md
@@ -125,18 +125,23 @@
library, e.g. `androidx.room:room-testing`
* `-core` for a low-level artifact that *may* contain public APIs but is
primarily intended for use by other libraries in the group
-* `-ktx` for an Kotlin artifact that exposes idiomatic Kotlin APIs as an
- extension to a Java-only library (see
- [additional -ktx guidance](#module-ktx))
+* `-common` for a low-level, platform-agnostic Kotlin multi-platform artifact
+ intended for both client use and use by other libraries in the group
+* `-ktx` for a Kotlin artifact that exposes idiomatic Kotlin APIs as an
+ extension to a Java-only library. Note that new modules should be written in
+ Kotlin rather than using `-ktx` artifacts.
* `-samples` for sample code which can be inlined in documentation (see
[Sample code in Kotlin modules](#sample-code-in-kotlin-modules)
* `-<third-party>` for an artifact that integrates an optional third-party API
- surface, e.g. `-proto` or `-rxjava2`. Note that a major version is included
- in the sub-feature name for third-party API surfaces where the major version
- indicates binary compatibility (only needed for post-1.x).
+ surface, e.g. `-proto`, `-guava`, or `-rxjava2`. This is common for Kotlin
+ libraries adapting their async APIs for Java clients. Note that a major
+ version is included in the sub-feature name (ex. `rxjava3`) for third-party
+ API surfaces where the major version indicates binary compatibility (only
+ needed for post-1.x).
Artifacts **should not** use `-impl` or `-base` to indicate that a library is an
-implementation detail shared within the group. Instead, use `-core`.
+implementation detail shared within the group. Instead, use `-core` or `-common`
+as appropriate.
#### Splitting existing modules
@@ -263,21 +268,24 @@
external developers and should be considered a last-resort when backporting
behavior is not feasible.
-### Kotlin extension `-ktx` libraries {#module-ktx}
+### Extension libraries (`-ktx`, `-guava`, etc.) {#module-ktx}
New libraries should prefer Kotlin sources with built-in Java compatibility via
-`@JvmName` and other affordances of the Kotlin language; however, existing Java
-sourced libraries may benefit from extending their API surface with
-Kotlin-friendly APIs in a `-ktx` library.
+`@JvmName` and other affordances of the Kotlin language. They may optionally
+expose framework- or language-specific extension libraries like `-guava` or
+`-rxjava3`.
-A Kotlin extension library **may only** provide extensions for a single base
-library's API surface and its name **must** match the base library exactly. For
-example, `work:work-ktx` may only provide extensions for APIs exposed by
-`work:work`.
+Existing Java-sourced libraries may benefit from extending their API surface
+with Kotlin-friendly APIs in a `-ktx` extension library.
+
+Extension libraries **may only** provide extensions for a single base library's
+API surface and its name **must** match the base library exactly. For example,
+`work:work-ktx` may only provide extensions for APIs exposed by `work:work`.
Additionally, an extension library **must** specify an `api`-type dependency on
the base library and **must** be versioned and released identically to the base
library.
-Kotlin extension libraries *should not* expose new functionality; they should
-only provide Kotlin-friendly versions of existing Java-facing functionality.
+Extension libraries *should not* expose new functionality; they should only
+provide language- or framework-friendly versions of existing library
+functionality.
diff --git a/docs/api_guidelines/resources.md b/docs/api_guidelines/resources.md
index 3290271..2220c81 100644
--- a/docs/api_guidelines/resources.md
+++ b/docs/api_guidelines/resources.md
@@ -183,7 +183,7 @@
* never invoked.
*/
public final class MetadataHolderService {
- private MetadataHolderService() {}
+ public MetadataHolderService() {}
@Override
public IBinder onBind(Intent intent) {
diff --git a/docs/benchmarking.md b/docs/benchmarking.md
index f138231..ae5e3d6 100644
--- a/docs/benchmarking.md
+++ b/docs/benchmarking.md
@@ -9,7 +9,7 @@
This page is for MICRO benchmarks measuring CPU performance of small sections of
code. If you're looking for measuring startup or jank, see the guide for
-MACRObenchmarks [here](macrobenchmarking).
+MACRObenchmarks [here](/company/teams/androidx/macrobenchmarking.md).
### Writing the benchmark
diff --git a/docs/index.md b/docs/index.md
index a34d30b..6408bc9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,10 +14,11 @@
simplify and unify platform features, and other new features that target
developer pain points.
-For frequently asked questions, see the [General FAQ](faq.md).
+For frequently asked questions, see the
+[General FAQ](/company/teams/androidx/faq.md).
-To get started developing in AndroidX, see the [Getting started](onboarding.md)
-guide.
+To get started developing in AndroidX, see the
+[Getting started](/company/teams/androidx/onboarding.md) guide.
## Quick links
@@ -27,4 +28,4 @@
[public Issue Tracker component](http://issuetracker.google.com/issues/new?component=192731&template=842428)
for duplicates first, then file against the appropriate sub-component according
to the library package or infrastructure system. Learn more at
-[Issue tracking](onboarding.md#tracking-bugs).
+[Issue tracking](/company/teams/androidx/onboarding.md#tracking-bugs).
diff --git a/docs/issue_tracking.md b/docs/issue_tracking.md
index e8b9c84..f33022c 100644
--- a/docs/issue_tracking.md
+++ b/docs/issue_tracking.md
@@ -100,13 +100,13 @@
## Issue lifecycle
1. When an issue is reported, it is set to `Assigned` status for default
- assignee (typically the [library owner](owners.md)) with a priority of
- **P4**.
+ assignee (typically the [library owner](/company/teams/androidx/owners.md))
+ with a priority of **P4**.
1. Once an issue has been triaged by the assignee, its priority will be raised
from **P4** according to severity.
1. The issue may still be reassigned at this point.
- [Bug bounty](onboarding.md#bug-bounty) issues are likely to change
- assignees.
+ [Bug bounty](/company/teams/androidx/onboarding.md#bug-bounty) issues are
+ likely to change assignees.
1. A status of **Accepted** means the assignee is actively working on the
issue.
1. A status of **Fixed** means that the issue has been resolved in the
diff --git a/docs/kdoc_guidelines.md b/docs/kdoc_guidelines.md
index 0d7469c..fdea07c 100644
--- a/docs/kdoc_guidelines.md
+++ b/docs/kdoc_guidelines.md
@@ -93,7 +93,7 @@
maintenance. You can use multiple samples per KDoc, with text in between
explaining what the samples are showing. For more information on using
`@sample`, see the
-[API guidelines](/company/teams/androidx/api_guidelines.md#sample-code-in-kotlin-modules).
+[API guidelines](/company/teams/androidx/api_guidelines/index.md#sample-code-in-kotlin-modules).
### Do not link to the same identifier inside documentation
diff --git a/docs/macrobenchmarking.md b/docs/macrobenchmarking.md
index a908ea9..86d0536 100644
--- a/docs/macrobenchmarking.md
+++ b/docs/macrobenchmarking.md
@@ -36,7 +36,7 @@
for macrobenchmark explains how to use the library. This page focuses on
specifics to writing library macrobenchmarks in the AndroidX repo. If you're
looking for measuring CPU perf of individual functions, see the guide for
-MICRObenchmarks [here](benchmarking).
+MICRObenchmarks [here](/company/teams/androidx/benchmarking.md).
### Writing the benchmark
diff --git a/docs/manual_prebuilts_dance.md b/docs/manual_prebuilts_dance.md
index 0439d44..4dcbf63 100644
--- a/docs/manual_prebuilts_dance.md
+++ b/docs/manual_prebuilts_dance.md
@@ -1,7 +1,8 @@
# The Manual Prebuilts Dance™
-NOTE There is also a [script](releasing_detailed.md#the-prebuilts-dance™) that
-automates this step.
+NOTE There is also a
+[script](/company/teams/androidx/releasing_detailed.md#the-prebuilts-dance™)
+that automates this step.
Public-facing Jetpack library docs are built from prebuilts to reconcile our
monolithic docs update process with our independently-versioned library release
diff --git a/docs/onboarding.md b/docs/onboarding.md
index 56fd9f3..8c5cef7 100644
--- a/docs/onboarding.md
+++ b/docs/onboarding.md
@@ -6,9 +6,11 @@
make simple changes in Android Studio, and upload commits to Gerrit for review.
This page does **not** cover best practices for the content of changes. Please
-see [Life of a Jetpack Feature](loaf.md) for details on developing and releasing
-a library, [API Guidelines](api_guidelines/index.md) for best practices
-regarding public APIs and an overview of the constraints placed on changes.
+see [Life of a Jetpack Feature](/company/teams/androidx/loaf.md) for details on
+developing and releasing a library,
+[API Guidelines](/company/teams/androidx/api_guidelines/index.md) for best
+practices regarding public APIs and an overview of the constraints placed on
+changes.
## Workstation setup {#setup}
@@ -220,46 +222,52 @@
normally would for application or library development -- right-click on a test
or sample to run or debug it, search through classes, and so on.
-If you get a “Unregistered VCS root detected” message, click “Add root” to
-enable the Git/VCS integration for Android Studio.
-
-If you see any errors (red underlines), click Gradle's elephant button in the
-toolbar ("Sync Project with Gradle Files") and they should resolve once the
-build completes.
-
> NOTE: You should choose "Use project SDK" when prompted by Studio. If you
> picked "Android Studio SDK" by mistake, don't panic! You can fix this by
> opening `File > Project Structure > Platform Settings > SDKs` and manually
> setting the Android SDK home path to
> `<project-root>/prebuilts/fullsdk-<platform>`.
-> NOTE: If Android Studio's UI looks scaled up, ex. twice the size it should be,
-> you may need to add the following line to your `studio64.vmoptions` file using
-> `Help -> Edit Custom VM Options`:
->
-> ```
-> -Dsun.java2d.uiScale.enabled=false
-> ```
+### Troubleshooting {#studio-troubleshooting}
-> NOTE: We are aware of a bug where running `./studiow` does not result in
-> Android Studio application being launched.
+* If you've updated to macOS Ventura and receive a "App is damaged and cannot
+ be opened" message when running Studio, *do not* move the app to the Trash.
+ Cancel out of the dialog and open macOS `System Settings > Gatekeeper`, look
+ for `"Android Studio" was blocked`, and click `Open Anyway` to grant an
+ exception. Alternatively, you can navigate to the Studio `.app` file under
+ `frameworks/support/studio` and run it once using `Control+Click` and `Open`
+ to automatically grant an exception.
+* If you've updated to macOS Ventura and receive a "xcrun: error: invalid
+ active developer path" message when running Studio, reinstall Xcode using
+ `xcode-select --install`. If that does not work, you will need to download
+ Xcode.
+* If you get a “Unregistered VCS root detected” message, click “Add root” to
+ enable the Git/VCS integration for Android Studio.
+* If you see any errors (red underlines), click Gradle's elephant button in
+ the toolbar (or `File > Sync Project with Gradle Files`) and they should
+ resolve once the build completes.
+* If you run `./studiow` with a new project set but you're still seeing the
+ old project set in `Project`, use `File > Sync Project with Gradle Files` to
+ force a re-sync.
+* If Android Studio's UI looks scaled up, ex. twice the size it should be, you
+ may need to add the following line to your `studio64.vmoptions` file using
+ `Help > Edit Custom VM Options`: `-Dsun.java2d.uiScale.enabled=false`
+* If you don't see a specific Gradle task listed in Studio's Gradle pane,
+ check the following:
+ * Studio might be running a different project subset than the one
+ intended. For example, `./studiow main` only loads the `main` set of
+ androidx projects; run `./studiow compose` to load the tasks specific to
+ Compose.
+ * Gradle tasks aren't being loaded. Under Studio's settings =>
+ Experimental, make sure that "Do not build Gradle task list during
+ Gradle sync" is unchecked. Note that unchecking this can reduce Studio's
+ performance.
If in the future you encounter unexpected errors in Studio and you want to check
for the possibility it is due to some incorrect settings or other generated
files, you can run `./studiow --clean main <project subset>` or `./studiow
--reinstall <project subset>` to clean generated files or reinstall Studio.
-> Tip: If you don't see a specific Gradle task listed in Studio's Gradle pane,
-> check the following:
->
-> * Studio might be running a different project subset than the one intended.
-> For example, `./studiow main` only loads the `main` set of androidx
-> projects; run `./studiow compose` to load the tasks specific to Compose.
->
-> * Gradle tasks aren't being loaded. Under Studio's settings => Experimental,
-> make sure that "Do not build Gradle task list during Gradle sync" is
-> unchecked. (Note that unchecking this can reduce Studio's performance)
-
### Enabling Compose `@Preview` annotation previews
Add the following dependencies to your project's `build.gradle`:
@@ -372,9 +380,8 @@
./gradlew core:core:assemble --strict
```
-To build every module, run the Lint verifier, verify the public API surface, and
-generate the local Maven repository artifact, use the `createArchive` Gradle
-task:
+To build every module and generate the local Maven repository artifact, use the
+`createArchive` Gradle task:
```shell
./gradlew createArchive
@@ -468,45 +475,20 @@
NOTE `./gradlew tasks` always has the canonical task information! When in doubt,
run `./gradlew tasks`
-#### Javadocs
-
-To build API reference docs for tip-of-tree Java source code, run the Gradle
-task:
-
-```
-./gradlew doclavaDocs
-```
-
-Places the documentation in
-`{androidx-main}/out/androidx/docs-tip-of-tree/build/javadoc`
-
-#### KotlinDocs
-
-To build API reference docs for tip-of-tree Kotlin source code, run the Gradle
-task:
-
-```
-./gradlew dokkaKotlinDocs
-```
-
-Places the documentation in
-`{androidx-main}/out/androidx/docs-tip-of-tree/build/dokkaKotlinDocs`
-
-#### Dackka docs
+#### Generate Docs
To build API reference docs for both Java and Kotlin source code using Dackka,
run the Gradle task:
```
-./gradlew dackkaDocs
+./gradlew docs
```
Location of generated refdocs:
* docs-public (what is published to DAC):
- `{androidx-main}/out/androidx/docs-public/build/dackkaDocs`
-* docs-tip-of-tree:
- `{androidx-main}/out/androidx/docs-tip-of-tree/build/dackkaDocs`
+ `{androidx-main}/out/androidx/docs-public/build/docs`
+* docs-tip-of-tree: `{androidx-main}/out/androidx/docs-tip-of-tree/build/docs`
The generated docs are plain HTML pages with links that do not work locally.
These issues are fixed when the docs are published to DAC, but to preview a
@@ -532,40 +514,21 @@
[d.android.com](http://d.android.com), run the Gradle command:
```
-./gradlew zipDoclavaDocs
+./gradlew zipDocs
```
-This will create the artifact
-`{androidx-main}/out/dist/doclava-public-docs-0.zip`. This command builds docs
-based on the version specified in
+This will create the artifact `{androidx-main}/out/dist/docs-public-0.zip`. This
+command builds docs based on the version specified in
`{androidx-main-checkout}/frameworks/support/docs-public/build.gradle` and uses
the prebuilt checked into
`{androidx-main-checkout}/prebuilts/androidx/internal/androidx/`. We
colloquially refer to this two step process of (1) updating `docs-public` and
(2) checking in a prebuilt artifact into the prebuilts directory as
-[The Prebuilts Dance](releasing_detailed.md#the-prebuilts-dance™). So, to build
-javadocs that will be published to
+[The Prebuilts Dance](/company/teams/androidx/releasing_detailed.md#the-prebuilts-dance™).
+So, to build javadocs that will be published to
https://developer.android.com/reference/androidx/packages, both of these steps
need to be completed.
-Once you done the above steps, Kotlin docs will also be generated, with the only
-difference being that we use the Gradle command:
-
-```
-./gradlew zipDokkaDocs
-```
-
-This will create the artifact `{androidx-main}/out/dist/dokka-public-docs-0.zip`
-
-To generate a zip artifact for both Java and Kotlin source code using Dackka:
-
-```
-./gradlew zipDackkaDocs
-```
-
-This will create the artifact
-`{androidx-main}/out/dist/dackka-public-docs-0.zip`
-
### Updating public APIs {#updating-public-apis}
Public API tasks -- including tracking, linting, and verifying compatibility --
@@ -595,6 +558,10 @@
If you change the public APIs without updating the API file, your module will
still build **but** your CL will fail Treehugger presubmit checks.
+NOTE The `updateApi` task does not generate versioned API files (e.g.
+`1.0.0-beta01.txt`) during a library's `alpha`, `rc` or stable cycles. The task
+will always generate `current.txt` API files.
+
#### What are all these files in `api/`? {#updating-public-apis-glossary}
Historical API surfaces are tracked for compatibility and docs generation
@@ -604,11 +571,11 @@
* `<version>.txt`: Public API surface, tracked for compatibility
* `restricted_<version>.txt`: `@RestrictTo` API surface, tracked for
compatibility where necessary (see
- [Restricted APIs](api_guidelines/index.md#restricted-api))
+ [Restricted APIs](/company/teams/androidx/api_guidelines/index.md#restricted-api))
* `public_plus_experimental_<version>.txt`: Public API surface plus
`@RequiresOptIn` experimental API surfaces used for documentation (see
- [Experimental APIs](api_guidelines/index.md#experimental-api)) and API
- review
+ [Experimental APIs](/company/teams/androidx/api_guidelines/index.md#experimental-api))
+ and API review
### Release notes & the `Relnote:` tag {#relnote}
@@ -794,7 +761,8 @@
are referencing new framework APIs, you will need to wait for the framework
changes to land in an SDK build (or build it yourself) and then land in both
prebuilts/fullsdk and prebuilts/sdk. See
-[Updating SDK prebuilts](playbook.md#prebuilts-fullsdk) for more information.
+[Updating SDK prebuilts](/company/teams/androidx/playbook.md#prebuilts-fullsdk)
+for more information.
#### How do I handle refactoring a framework API referenced from a library?
@@ -836,8 +804,8 @@
or `support-\*-demos` (e.g. `support-v4-demos` or `support-leanback-demos`). You
can run them by clicking `Run > Run ...` and choosing the desired application.
-See the [Testing](testing.md) page for more resources on writing, running, and
-monitoring tests.
+See the [Testing](/company/teams/androidx/testing.md) page for more resources on
+writing, running, and monitoring tests.
### AVD Manager
@@ -937,7 +905,7 @@
### Using in a Gradle build
-To make these artifacts visible to Gradle, you need to add it as a respository:
+To make these artifacts visible to Gradle, you need to add it as a repository:
```groovy
allprojects {
@@ -960,7 +928,7 @@
to use a snapshot artifact, the version in your `build.gradle` will need to be
updated to `androidx.<groupId>:<artifactId>:X.Y.Z-SNAPSHOT`
-For example, to use the `core:core:1.2.0-SHAPSHOT` snapshot, you would add the
+For example, to use the `core:core:1.2.0-SNAPSHOT` snapshot, you would add the
following to your `build.gradle`:
```
@@ -1032,7 +1000,7 @@
full m2repository to avoid version issues of other modules. You can either take
the unzipped directory from
`<path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip`, or from
-`<path-to-checkout>/out/androidx/build/support_repo/` after buiding `androidx`.
+`<path-to-checkout>/out/androidx/build/support_repo/` after building `androidx`.
Here is an example of replacing the RecyclerView module:
```shell
@@ -1047,7 +1015,8 @@
### How do I measure library size? {#library-size}
Method count and bytecode size are tracked in CI
-[alongside benchmarks](benchmarking.md#monitoring) to detect regressions.
+[alongside benchmarks](/company/teams/androidx/benchmarking.md#monitoring) to
+detect regressions.
For local measurements, use the `:reportLibraryMetrics` task. For example:
@@ -1072,3 +1041,21 @@
[Overview page](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary)
includes content from
[compose-runtime-documentation.md](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/compose-runtime-documentation.md).
+
+### How do I enable MultiDex for my library?
+
+Go to your project/app level build.gradle file, and add
+
+```
+android {
+ defaultConfig {
+ multiDexEnabled = true
+ }
+}
+```
+
+as well as `androidTestImplementation(libs.multidex)` to the dependenices block.
+
+If you want it enabled for the application and not test apk, add
+`implementation(libs.multidex)` to the dependencies block instead. Any prior
+failures may not re-occur now that the software is multi-dexed. Rerun the build.
diff --git a/docs/open_source.md b/docs/open_source.md
index 173d1e77..392dc34 100644
--- a/docs/open_source.md
+++ b/docs/open_source.md
@@ -29,7 +29,7 @@
#### Closed-source dependencies
In specific cases, libraries *may* include closed-source dependencies. See the
-[Open-source compatibility](api_guidelines/dependencies.md#dependencies-aosp)
+[Open-source compatibility](/company/teams/androidx/api_guidelines/index.md#dependencies-aosp)
section of the API Guidelines for implementation details.
### Examples of products that are _not_ open-source
diff --git a/docs/principles.md b/docs/principles.md
index 523c87f..a2d2a42 100644
--- a/docs/principles.md
+++ b/docs/principles.md
@@ -60,8 +60,9 @@
to choose between a variety of services as the backing implementation
- Comply with AndroidX checks and policies such as API tracking and style
checks
-- See [Integrating proprietary components](open_source.md) for guidance on
- using closed-source and proprietary libraries and services
+- See
+ [Integrating proprietary components](/company/teams/androidx/open_source.md)
+ for guidance on using closed-source and proprietary libraries and services
### 6. Written using language-idiomatic APIs
diff --git a/docs/team.md b/docs/team.md
index 0c61aa4b..0149376 100644
--- a/docs/team.md
+++ b/docs/team.md
@@ -4,7 +4,7 @@
<!--*
# Document freshness: For more information, see go/fresh-source.
-freshness: { owner: 'alanv' reviewed: '2022-06-10' }
+freshness: { owner: 'alanv' reviewed: '2022-12-09' }
*-->
## What we do
diff --git a/docs/testability.md b/docs/testability.md
index 45d4b6d..5a1a773 100644
--- a/docs/testability.md
+++ b/docs/testability.md
@@ -39,11 +39,11 @@
improvement.
To get started with sample code, see
-[Sample code in Kotlin modules](api_guidelines.md#sample-code-in-kotlin-modules)
+[Sample code in Kotlin modules](/company/teams/androidx/api_guidelines/index.md#sample-code-in-kotlin-modules)
for information on writing samples that can be referenced from API reference
documentation or
-[Project directory structure](api_guidelines.md#module-structure) for module
-naming guidelines if you'd like to create a basic test app.
+[Project directory structure](/company/teams/androidx/api_guidelines/index.md#module-structure)
+for module naming guidelines if you'd like to create a basic test app.
### Avoiding side-effects {#side-effects}
@@ -308,8 +308,9 @@
state has been reached or the requested action has been completed. This might
mean adding listeners, callbacks, `ListenableFuture`s, or `LiveData`.
-See [Asynchronous work](api_guidelines.md#async) in the API Guidelines for more
-information on exposing the state of asynchronous work to clients.
+See [Asynchronous work](/company/teams/androidx/api_guidelines/index.md#async)
+in the API Guidelines for more information on exposing the state of asynchronous
+work to clients.
### Calling `Thread.sleep()` as a synchronization barrier
diff --git a/docs/testing.md b/docs/testing.md
index b59a240..b6fde3a 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -30,7 +30,7 @@
NOTE For best practices on writing libraries in a way that makes it easy for end
users -- and library developers -- to write tests, see the
-[Testability](testability.md) guide.
+[Testability](/company/teams/androidx/testability.md) guide.
### What gets tested, and when {#affected-module-detector}
@@ -255,7 +255,7 @@
* Validation of API usability and developer experience, when paired with a use
case or critical user journey
* Sample documentation, when embedded into API reference docs using the
- [`@sample` and `@Sampled` annotations](api_guidelines.md#sample-usage)
+ [`@sample` and `@Sampled` annotations](/company/teams/androidx/api_guidelines/index.md#sample-usage)
### Legacy test apps {#testapps-legacy}
@@ -273,4 +273,4 @@
AndroidX supports benchmarking - locally with Studio/Gradle, and continuously in
post-submit. For more information on how to create and run benchmarks, see
-[Benchmarking](benchmarking.md).
+[Benchmarking](/company/teams/androidx/benchmarking.md).
diff --git a/docs/versioning.md b/docs/versioning.md
index b7b5a1a..12434a5 100644
--- a/docs/versioning.md
+++ b/docs/versioning.md
@@ -6,24 +6,31 @@
including the expectations at each cycle and criteria for moving to the next
cycle or SemVer revision.
-## Semantic versioning
+## Semantic versioning and binary compatibility {#semver}
Artifacts follow strict [semantic versioning](http://semver.org) for binary
-compatibility with an added inter-version sequence of pre-release revisions. The
-version for a finalized release artifact will follow the format
+compatibility with an added inter-version sequence of pre-release revisions.
+Versions for finalized release artifacts, which are available on
+[Google Maven](https://maven.google.com) will follow the format
`<major>.<minor>.<bugfix>` with an optional `-<alpha|beta|rc><nn>` suffix.
-Internal or nightly releases (via [androidx.dev](http://androidx.dev)) use the
-`-SNAPSHOT` suffix.
+Internal or nightly releases, which are available on
+[androidx.dev](http://androidx.dev), use the `-SNAPSHOT` suffix.
-### Source compatibility
+### Behavioral and source compatibility {#compat}
-Libraries are encouraged -- but not required -- to preserve source compatibility
-across minor versions. Strictly requiring source compatibility would require
-major version bumps when implementing quality-of-life improvements such as
-nullability annotations or generics, which would be
-[disruptive to the library ecosystem](#major-implications).
+Libraries are required to preserve *behavioral compatibility* -- APIs must
+behave as described in their documentation -- across minor versions. Special
+consideration must also be made for changes to undocumented behavior, as
+developers may have made their own assumptions about API contracts based on
+observed behavior.
-### Notation
+Libraries are strongly encouraged to preserve *source compatibility* across
+minor versions. Strictly requiring source compatibility would require major
+version bumps when implementing quality-of-life improvements such as nullability
+annotations or generics, which would be [disruptive](#major-implications) to the
+library ecosystem.
+
+### Notation {#notation}
Major (`x.0.0`)
: An artifact's major version indicates a guaranteed forward-compatibility
@@ -33,7 +40,7 @@
Minor (`1.x.0`)
: Minor indicates compatible public API changes. This number is incremented
when APIs are added, including the addition of
- [`@Deprecated` annotations](api_guidelines.md#deprecation-and-removal).
+ [`@Deprecated` annotations](/company/teams/androidx/api_guidelines/index.md#deprecation-and-removal).
Binary compatibility must be preserved between minor version changes.
Bugfix (`1.0.x`)
@@ -41,7 +48,7 @@
taken to ensure that existing clients are not broken, including clients that
may have been working around long-standing broken behavior.
-#### Pre-release cycles
+#### Pre-release cycles {#prerelease}
Alpha (`1.0.0-alphaXX`)
: Feature development and API stabilization phase.
@@ -231,8 +238,9 @@
`publish=true` or create an `api` directory) and remain enabled
* May add/remove APIs within `alpha` cycle, but deprecate/remove cycle is
strongly recommended.
- * May use [experimental APIs](api_guidelines.md#experimental-api) across
- same-version group boundaries
+ * May use
+ [experimental APIs](/company/teams/androidx/api_guidelines/index.md#experimental-api)
+ across same-version group boundaries
* Testing
* All changes **should** be accompanied by a `Test:` stanza
* All pre-submit and post-submit tests are passing
@@ -264,13 +272,13 @@
* All APIs from alpha undergoing deprecate/remove cycle must be removed
* The final removal of a `@Deprecated` API should occur in alpha, not
in beta
- * Must not use [experimental APIs](api_guidelines.md#experimental-api)
+ * Must not use
+ [experimental APIs](/company/teams/androidx/api_guidelines#experimental-api)
across same-version group boundaries
* Testing
* All public APIs are tested
* All pre-submit and post-submit tests are enabled (e.g. all suppressions
are removed) and passing
- * Your library passes `./gradlew library:checkReleaseReady`
* Use of experimental Kotlin features (e.g. `@OptIn`) must be audited for
stability
* All dependencies are `beta`, `rc`, or stable
@@ -336,22 +344,19 @@
- The version of your library listed in `androidx-main` should *always* be
higher than the version publically available on Google Maven. This allows us
to do proper version tracking and API tracking.
-
- Version increments must be done before the CL cutoff date (aka the build cut
date).
-
- **Increments to the next stability suffix** (like `alpha` to `beta`) should
- be handled by the library owner, with the Jetpack TPM (nickanthony@) CC'd
+ be handled by the library owner, with the Jetpack TPM (natnaelbelay@) CC'd
for API+1.
-
- Version increments in release branches will need to follow the guide
- [How to update your version on a release branch](release_branches.md#update-your-version)
-
+ [How to update your version on a release branch](/company/teams/androidx/release_branches.md#update-your-version)
- When you're ready for `rc01`, the increment to `rc01` should be done in
`androidx-main` and then your release branch should be snapped to that
- build. See the guide [Snap your release branch](release_branches.md#snap) on
- how to do this. After the release branch is snapped to that build, you will
- need to update your version in `androidx-main` to `alpha01` of the next
+ build. See the guide
+ [Snap your release branch](/company/teams/androidx/release_branches.md#snap)
+ on how to do this. After the release branch is snapped to that build, you
+ will need to update your version in `androidx-main` to `alpha01` of the next
minor (or major) version.
### How to update your version
@@ -369,9 +374,10 @@
## `-ktx` Modules {#ktx}
-[Kotlin extension libraries](api_guidelines.md#module-ktx) (`-ktx`) follow the
-same versioning requirements as other libraries, but with one exception: they
-must match the version of the Java libraries that they extend.
+[Kotlin extension libraries](/company/teams/androidx/api_guidelines/index.md#module-ktx)
+(`-ktx`) follow the same versioning requirements as other libraries, but with
+one exception: they must match the version of the Java libraries that they
+extend.
For example, let's say you are developing a Java library
`androidx.foo:foo-bar:1.1.0-alpha01` and you want to add a Kotlin extension
@@ -389,10 +395,6 @@
Generally, these occur during the batched bi-weekly (every 2 weeks) release
because all tip-of-tree dependencies will need to be released too.
-### Are there restrictions on when or how often an alpha can ship?
-
-Nope.
-
### Can alpha work (ex. for the next Minor release) occur in the primary development branch during beta API lockdown?
No. This is by design. Focus should be spent on improving the Beta version and
@@ -405,5 +407,5 @@
### How often can a beta release?
-As often as needed, however, releases outside of the bi-weekly (every 2 weeks)
+As often as needed; however, releases outside of the bi-weekly (every 2 weeks)
release will need to get approval from the TPM (natnaelbelay@).
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
index a155641..45e6dbd 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -17,7 +17,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21"/>
- <application>
+ <application
+ android:label="Emoji Picker Sample App" >
<activity android:name=".MainActivity" android:exported="true"
android:theme="@style/MyPinkTheme">
<!-- Handle Google app icon launch. -->
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
index 89ce1c3..32ff475 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
@@ -18,12 +18,14 @@
<resources>
<style name="MyPinkTheme" parent="Theme.AppCompat.DayNight" >
- <!-- The color for selected headers -->
+ <!-- The color for selected category icons -->
<item name="colorAccent">#696969</item>
- <!-- The color for unselected headers -->
+ <!-- The color for unselected category icons -->
<item name="colorControlNormal">#FFC0CB</item>
<!-- The color for all text -->
<item name="android:textColorPrimary">#696969</item>
+ <!-- The text size for category text -->
+ <item name="android:textSize">12dp</item>
<!-- The color for variant popup background -->
<item name="colorButtonNormal">#FFC0CB</item>
</style>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
index c7bc5af..354bc25 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
@@ -31,6 +31,5 @@
android:gravity="center_vertical|start"
android:letterSpacing="0.1"
android:importantForAccessibility="yes"
- android:textColor="?android:attr/textColorPrimary"
- android:textSize="12dp" />
+ android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>
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 eec343d..e9529a9 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
@@ -34,7 +34,6 @@
android:layout_height="match_parent"
android:background="@drawable/ripple_emoji_view"
android:importantForAccessibility="yes"
- android:textSize="30dp"
android:layout_margin="1dp" />
</FrameLayout>
<ImageView
diff --git a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
index 5ab1dda..504a5af 100644
--- a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
+++ b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/FragmentViewModelLazyTest.kt
@@ -99,18 +99,17 @@
enableSavedStateHandles()
}
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return SavedStateViewModelFactory()
- }
+ override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
- override fun getDefaultViewModelCreationExtras(): CreationExtras {
- val extras = MutableCreationExtras()
- extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
- extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
- extras[VIEW_MODEL_STORE_OWNER_KEY] = this
- extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
- return extras
- }
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() {
+ val extras = MutableCreationExtras()
+ extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
+ extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
+ extras[VIEW_MODEL_STORE_OWNER_KEY] = this
+ extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+ return extras
+ }
}
class TestViewModel : ViewModel()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
index c68e779..6a83cf8 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
@@ -183,7 +183,6 @@
}
public class FragmentWithFactoryOverride : StrictViewFragment() {
- public override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory =
- FakeViewModelProviderFactory()
+ public override val defaultViewModelProviderFactory = FakeViewModelProviderFactory()
}
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7a5b69b..c1a5e5f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -161,7 +161,7 @@
kotlinCoroutinesRx3 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx3", version.ref = "kotlinCoroutines" }
kotlinDaemonEmbeddable = { module = "org.jetbrains.kotlin:kotlin-daemon-embeddable", version.ref = "kotlin" }
kotlinKlibCommonizer = { module = "org.jetbrains.kotlin:kotlin-klib-commonizer", version.ref = "kotlin" }
-kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.5.0" }
+kotlinMetadataJvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.6.0" }
kotlinSerializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinSerialization" }
kotlinSerializationProtobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinSerialization" }
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
diff --git a/libraryversions.toml b/libraryversions.toml
index 4e3538a..9160d928 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -12,7 +12,7 @@
BENCHMARK = "1.2.0-alpha10"
BIOMETRIC = "1.2.0-alpha06"
BLUETOOTH = "1.0.0-alpha01"
-BROWSER = "1.5.0-beta02"
+BROWSER = "1.5.0-rc01"
BUILDSRC_TESTS = "1.0.0-alpha01"
CAMERA = "1.3.0-alpha03"
CAMERA_PIPE = "1.0.0-alpha01"
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
index bc46abe..c067c51 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
@@ -271,5 +271,5 @@
val factory = FakeViewModelProviderFactory()
override fun getViewModelStore(): ViewModelStore = store
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory = factory
+ override val defaultViewModelProviderFactory = factory
}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/current.ignore b/lifecycle/lifecycle-viewmodel-savedstate/api/current.ignore
new file mode 100644
index 0000000..3bd1b51
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>):
+ Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>)
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>, androidx.lifecycle.viewmodel.CreationExtras):
+ Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>,androidx.lifecycle.viewmodel.CreationExtras)
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt
index 35fdaaa..c030c8a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt
@@ -3,10 +3,8 @@
public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
ctor public AbstractSavedStateViewModelFactory();
- ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner, android.os.Bundle?);
- method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>, androidx.lifecycle.viewmodel.CreationExtras);
- method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>);
- method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String, Class<T!>, androidx.lifecycle.SavedStateHandle);
+ ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
}
public final class SavedStateHandle {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt
index 35fdaaa..c030c8a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/public_plus_experimental_current.txt
@@ -3,10 +3,8 @@
public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
ctor public AbstractSavedStateViewModelFactory();
- ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner, android.os.Bundle?);
- method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>, androidx.lifecycle.viewmodel.CreationExtras);
- method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>);
- method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String, Class<T!>, androidx.lifecycle.SavedStateHandle);
+ ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
}
public final class SavedStateHandle {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.ignore b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.ignore
new file mode 100644
index 0000000..3bd1b51
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>):
+ Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>)
+RemovedMethod: androidx.lifecycle.AbstractSavedStateViewModelFactory#create(Class<T>, androidx.lifecycle.viewmodel.CreationExtras):
+ Removed method androidx.lifecycle.AbstractSavedStateViewModelFactory.create(Class<T>,androidx.lifecycle.viewmodel.CreationExtras)
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt
index 35fdaaa..c030c8a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/api/restricted_current.txt
@@ -3,10 +3,8 @@
public abstract class AbstractSavedStateViewModelFactory implements androidx.lifecycle.ViewModelProvider.Factory {
ctor public AbstractSavedStateViewModelFactory();
- ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner, android.os.Bundle?);
- method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>, androidx.lifecycle.viewmodel.CreationExtras);
- method public final <T extends androidx.lifecycle.ViewModel> T create(Class<T!>);
- method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String, Class<T!>, androidx.lifecycle.SavedStateHandle);
+ ctor public AbstractSavedStateViewModelFactory(androidx.savedstate.SavedStateRegistryOwner owner, android.os.Bundle? defaultArgs);
+ method protected abstract <T extends androidx.lifecycle.ViewModel> T create(String key, Class<T> modelClass, androidx.lifecycle.SavedStateHandle handle);
}
public final class SavedStateHandle {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt
index c9d5d4e..c139d47 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/ViewModelsWithStateTest.kt
@@ -251,14 +251,16 @@
SavedStateRegistryOwner by ssrOwner,
HasDefaultViewModelProviderFactory {
- override fun getDefaultViewModelProviderFactory(): Factory {
- throw UnsupportedOperationException()
- }
+ override val defaultViewModelProviderFactory: Factory
+ get() {
+ throw UnsupportedOperationException()
+ }
- override fun getDefaultViewModelCreationExtras(): CreationExtras {
- val extras = MutableCreationExtras()
- extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
- extras[VIEW_MODEL_STORE_OWNER_KEY] = this
- return extras
- }
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() {
+ val extras = MutableCreationExtras()
+ extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
+ extras[VIEW_MODEL_STORE_OWNER_KEY] = this
+ return extras
+ }
}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.java
deleted file mode 100644
index dad9f6f..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.java
+++ /dev/null
@@ -1,148 +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.lifecycle;
-
-import static androidx.lifecycle.LegacySavedStateHandleController.attachHandleIfNeeded;
-import static androidx.lifecycle.ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY;
-
-import android.annotation.SuppressLint;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.lifecycle.viewmodel.CreationExtras;
-import androidx.savedstate.SavedStateRegistry;
-import androidx.savedstate.SavedStateRegistryOwner;
-
-/**
- * Skeleton of androidx.lifecycle.ViewModelProvider.KeyedFactory
- * that creates {@link SavedStateHandle} for every requested {@link androidx.lifecycle.ViewModel}.
- * The subclasses implement {@link #create(String, Class, SavedStateHandle)} to actually instantiate
- * {@code androidx.lifecycle.ViewModel}s.
- */
-public abstract class AbstractSavedStateViewModelFactory extends ViewModelProvider.OnRequeryFactory
- implements ViewModelProvider.Factory {
- static final String TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag";
-
- private SavedStateRegistry mSavedStateRegistry;
- private Lifecycle mLifecycle;
- private Bundle mDefaultArgs;
-
- /**
- * Constructs this factory.
- * <p>
- * When a factory is constructed this way, a component for which {@link SavedStateHandle} is
- * scoped must have called
- * {@link SavedStateHandleSupport#enableSavedStateHandles(SavedStateRegistryOwner)}.
- * See {@link SavedStateHandleSupport#createSavedStateHandle(CreationExtras)} docs for more
- * details.
- */
- public AbstractSavedStateViewModelFactory() {
- }
-
- /**
- * Constructs this factory.
- *
- * @param owner {@link SavedStateRegistryOwner} that will provide restored state for created
- * {@link androidx.lifecycle.ViewModel ViewModels}
- * @param defaultArgs values from this {@code Bundle} will be used as defaults by
- * {@link SavedStateHandle} passed in {@link ViewModel ViewModels}
- * if there is no previously saved state
- * or previously saved state misses a value by such key
- */
- @SuppressLint("LambdaLast")
- public AbstractSavedStateViewModelFactory(@NonNull SavedStateRegistryOwner owner,
- @Nullable Bundle defaultArgs) {
- mSavedStateRegistry = owner.getSavedStateRegistry();
- mLifecycle = owner.getLifecycle();
- mDefaultArgs = defaultArgs;
- }
-
- @NonNull
- @Override
- public final <T extends ViewModel> T create(@NonNull Class<T> modelClass,
- @NonNull CreationExtras extras) {
- String key = extras.get(VIEW_MODEL_KEY);
- if (key == null) {
- throw new IllegalStateException(
- "VIEW_MODEL_KEY must always be provided by ViewModelProvider");
- }
- // if a factory constructed in the old way use the old infra to create SavedStateHandle
- if (mSavedStateRegistry != null) {
- return create(key, modelClass);
- } else {
- return create(key, modelClass, SavedStateHandleSupport.createSavedStateHandle(extras));
- }
- }
-
- @NonNull
- private <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
- SavedStateHandleController controller = LegacySavedStateHandleController
- .create(mSavedStateRegistry, mLifecycle, key, mDefaultArgs);
- T viewmodel = create(key, modelClass, controller.getHandle());
- viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
- return viewmodel;
- }
-
- @NonNull
- @Override
- public final <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- // ViewModelProvider calls correct create that support same modelClass with different keys
- // If a developer manually calls this method, there is no "key" in picture, so factory
- // simply uses classname internally as as key.
- String canonicalName = modelClass.getCanonicalName();
- if (canonicalName == null) {
- throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
- }
- if (mLifecycle == null) {
- throw new UnsupportedOperationException(
- "AbstractSavedStateViewModelFactory constructed "
- + "with empty constructor supports only calls to "
- + "create(modelClass: Class<T>, extras: CreationExtras)."
- );
- }
- return create(canonicalName, modelClass);
- }
-
- /**
- * Creates a new instance of the given {@code Class}.
- * <p>
- *
- * @param key a key associated with the requested ViewModel
- * @param modelClass a {@code Class} whose instance is requested
- * @param handle a handle to saved state associated with the requested ViewModel
- * @param <T> The type parameter for the ViewModel.
- * @return a newly created ViewModels
- */
- @NonNull
- protected abstract <T extends ViewModel> T create(@NonNull String key,
- @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle);
-
- /**
- * @hide
- */
- @Override
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public void onRequery(@NonNull ViewModel viewModel) {
- // is need only for legacy path
- if (mSavedStateRegistry != null) {
- attachHandleIfNeeded(viewModel, mSavedStateRegistry, mLifecycle);
- }
- }
-}
-
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
new file mode 100644
index 0000000..6561594
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.lifecycle
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.lifecycle.LegacySavedStateHandleController.attachHandleIfNeeded
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryOwner
+
+/**
+ * Skeleton of androidx.lifecycle.ViewModelProvider.KeyedFactory
+ * that creates [SavedStateHandle] for every requested [ViewModel].
+ * The subclasses implement [create] to actually instantiate
+ * `androidx.lifecycle.ViewModel`s.
+ */
+public abstract class AbstractSavedStateViewModelFactory :
+ ViewModelProvider.OnRequeryFactory,
+ ViewModelProvider.Factory {
+
+ private var savedStateRegistry: SavedStateRegistry? = null
+ private var lifecycle: Lifecycle? = null
+ private var defaultArgs: Bundle? = null
+
+ /**
+ * Constructs this factory.
+ *
+ * When a factory is constructed this way, a component for which [SavedStateHandle] is
+ * scoped must have called
+ * [SavedStateHandleSupport.enableSavedStateHandles].
+ * See [CreationExtras.createSavedStateHandle] docs for more
+ * details.
+ */
+ constructor() {}
+
+ /**
+ * Constructs this factory.
+ *
+ * @param owner [SavedStateRegistryOwner] that will provide restored state for created
+ * [ViewModels][ViewModel]
+ * @param defaultArgs values from this `Bundle` will be used as defaults by
+ * [SavedStateHandle] passed in [ViewModels][ViewModel] if there is no
+ * previously saved state or previously saved state misses a value by such key
+ */
+ constructor(
+ owner: SavedStateRegistryOwner,
+ defaultArgs: Bundle?
+ ) {
+ savedStateRegistry = owner.savedStateRegistry
+ lifecycle = owner.lifecycle
+ this.defaultArgs = defaultArgs
+ }
+
+ /**
+ * Creates a new instance of the given `Class`.
+ *
+ * @param modelClass a `Class` whose instance is requested
+ * @param extras an additional information for this creation request
+ *
+ * @return a newly created ViewModel
+ *
+ * @throws IllegalStateException if no VIEW_MODEL_KEY provided by ViewModelProvider
+ */
+ public override fun <T : ViewModel> create(
+ modelClass: Class<T>,
+ extras: CreationExtras
+ ): T {
+ val key = extras[ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY]
+ ?: throw IllegalStateException(
+ "VIEW_MODEL_KEY must always be provided by ViewModelProvider"
+ )
+ // if a factory constructed in the old way use the old infra to create SavedStateHandle
+ return if (savedStateRegistry != null) {
+ create(key, modelClass)
+ } else {
+ create(key, modelClass, extras.createSavedStateHandle())
+ }
+ }
+
+ private fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
+ val controller = LegacySavedStateHandleController
+ .create(savedStateRegistry, lifecycle, key, defaultArgs)
+ val viewModel = create(key, modelClass, controller.handle)
+ viewModel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller)
+ return viewModel
+ }
+
+ /**
+ * Creates a new instance of the given `Class`.
+ *
+ * @param modelClass a `Class` whose instance is requested
+ *
+ * @return a newly created ViewModel
+ *
+ * @throws IllegalArgumentException if the given [modelClass] is local or anonymous class.
+ * @throws UnsupportedOperationException if AbstractSavedStateViewModelFactory constructed
+ * with empty constructor, therefore no [SavedStateRegistryOwner] available for lifecycle
+ */
+ public override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ // ViewModelProvider calls correct create that support same modelClass with different keys
+ // If a developer manually calls this method, there is no "key" in picture, so factory
+ // simply uses classname internally as as key.
+ val canonicalName = modelClass.canonicalName
+ ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
+ if (lifecycle == null) {
+ throw UnsupportedOperationException(
+ "AbstractSavedStateViewModelFactory constructed " +
+ "with empty constructor supports only calls to " +
+ "create(modelClass: Class<T>, extras: CreationExtras)."
+ )
+ }
+ return create(canonicalName, modelClass)
+ }
+
+ /**
+ * Creates a new instance of the given `Class`.
+ *
+ * @param key a key associated with the requested ViewModel
+ * @param modelClass a `Class` whose instance is requested
+ * @param handle a handle to saved state associated with the requested ViewModel
+ *
+ * @return the newly created ViewModel
+ </T> */
+ protected abstract fun <T : ViewModel> create(
+ key: String,
+ modelClass: Class<T>,
+ handle: SavedStateHandle
+ ): T
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ override fun onRequery(viewModel: ViewModel) {
+ // is need only for legacy path
+ if (savedStateRegistry != null) {
+ attachHandleIfNeeded(viewModel, savedStateRegistry, lifecycle)
+ }
+ }
+
+ internal companion object {
+ internal const val TAG_SAVED_STATE_HANDLE_CONTROLLER =
+ "androidx.lifecycle.savedstate.vm.tag"
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.java
deleted file mode 100644
index 33797c7..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.java
+++ /dev/null
@@ -1,56 +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.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.savedstate.SavedStateRegistry;
-
-final class SavedStateHandleController implements LifecycleEventObserver {
- private final String mKey;
- private boolean mIsAttached = false;
- private final SavedStateHandle mHandle;
-
- SavedStateHandleController(String key, SavedStateHandle handle) {
- mKey = key;
- mHandle = handle;
- }
-
- boolean isAttached() {
- return mIsAttached;
- }
-
- void attachToLifecycle(SavedStateRegistry registry, Lifecycle lifecycle) {
- if (mIsAttached) {
- throw new IllegalStateException("Already attached to lifecycleOwner");
- }
- mIsAttached = true;
- lifecycle.addObserver(this);
- registry.registerSavedStateProvider(mKey, mHandle.savedStateProvider());
- }
-
- @Override
- public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
- if (event == Lifecycle.Event.ON_DESTROY) {
- mIsAttached = false;
- source.getLifecycle().removeObserver(this);
- }
- }
-
- SavedStateHandle getHandle() {
- return mHandle;
- }
-}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.kt
new file mode 100644
index 0000000..d50f2c7
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleController.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.lifecycle
+
+import androidx.savedstate.SavedStateRegistry
+
+internal class SavedStateHandleController(
+ private val key: String,
+ val handle: SavedStateHandle
+) : LifecycleEventObserver {
+
+ var isAttached = false
+ private set
+
+ fun attachToLifecycle(registry: SavedStateRegistry, lifecycle: Lifecycle) {
+ check(!isAttached) { "Already attached to lifecycleOwner" }
+ isAttached = true
+ lifecycle.addObserver(this)
+ registry.registerSavedStateProvider(key, handle.savedStateProvider())
+ }
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event === Lifecycle.Event.ON_DESTROY) {
+ isAttached = false
+ source.lifecycle.removeObserver(this)
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel/api/current.txt b/lifecycle/lifecycle-viewmodel/api/current.txt
index e4f76f95..577a5be 100644
--- a/lifecycle/lifecycle-viewmodel/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/current.txt
@@ -9,6 +9,8 @@
public interface HasDefaultViewModelProviderFactory {
method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+ property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
}
public abstract class ViewModel {
diff --git a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
index e4f76f95..577a5be 100644
--- a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
@@ -9,6 +9,8 @@
public interface HasDefaultViewModelProviderFactory {
method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+ property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
}
public abstract class ViewModel {
diff --git a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
index e4f76f95..577a5be 100644
--- a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
@@ -9,6 +9,8 @@
public interface HasDefaultViewModelProviderFactory {
method public default androidx.lifecycle.viewmodel.CreationExtras getDefaultViewModelCreationExtras();
method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
+ property public default androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public abstract androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
}
public abstract class ViewModel {
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.java
deleted file mode 100644
index ff7539a..0000000
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.java
+++ /dev/null
@@ -1,48 +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.lifecycle;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.viewmodel.CreationExtras;
-
-/**
- * Interface that marks a {@link ViewModelStoreOwner} as having a default
- * {@link androidx.lifecycle.ViewModelProvider.Factory} for use with
- * {@link androidx.lifecycle.ViewModelProvider#ViewModelProvider(ViewModelStoreOwner)}.
- */
-public interface HasDefaultViewModelProviderFactory {
- /**
- * Returns the default {@link androidx.lifecycle.ViewModelProvider.Factory} that should be
- * used when no custom {@code Factory} is provided to the
- * {@link androidx.lifecycle.ViewModelProvider} constructors.
- *
- * @return a {@code ViewModelProvider.Factory}
- */
- @NonNull
- ViewModelProvider.Factory getDefaultViewModelProviderFactory();
-
- /**
- * Returns the default {@link CreationExtras} that should be passed into the
- * {@link ViewModelProvider.Factory#create(Class, CreationExtras)} when no overriding
- * {@link CreationExtras} were passed to the
- * {@link androidx.lifecycle.ViewModelProvider} constructors.
- */
- @NonNull
- default CreationExtras getDefaultViewModelCreationExtras() {
- return CreationExtras.Empty.INSTANCE;
- }
-}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.kt
new file mode 100644
index 0000000..3a896b1
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/HasDefaultViewModelProviderFactory.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.lifecycle
+
+import androidx.lifecycle.viewmodel.CreationExtras
+
+/**
+ * Interface that marks a [ViewModelStoreOwner] as having a default
+ * [ViewModelProvider.Factory] for use with [ViewModelProvider].
+ */
+interface HasDefaultViewModelProviderFactory {
+ /**
+ * Returns the default [ViewModelProvider.Factory] that should be
+ * used when no custom `Factory` is provided to the
+ * [ViewModelProvider] constructors.
+ */
+ val defaultViewModelProviderFactory: ViewModelProvider.Factory
+
+ /**
+ * Returns the default [CreationExtras] that should be passed into
+ * [ViewModelProvider.Factory.create] when no overriding
+ * [CreationExtras] were passed to the [ViewModelProvider] constructors.
+ */
+ val defaultViewModelCreationExtras: CreationExtras
+ get() = CreationExtras.Empty
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
index 06c9025..ba62b11 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.kt
@@ -109,7 +109,7 @@
*
*
* This method will use the
- * [default factory][HasDefaultViewModelProviderFactory.getDefaultViewModelProviderFactory]
+ * [default factory][HasDefaultViewModelProviderFactory.defaultViewModelProviderFactory]
* if the owner implements [HasDefaultViewModelProviderFactory]. Otherwise, a
* [NewInstanceFactory] will be used.
*/
diff --git a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
index f4423a9..2536918 100644
--- a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
@@ -137,9 +137,7 @@
assertThat(wasCalled[0]).isTrue()
wasCalled[0] = false
ViewModelProvider(object : ViewModelStoreOwnerWithCreationExtras() {
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return testFactory
- }
+ override val defaultViewModelProviderFactory = testFactory
})["customKey", ViewModel1::class.java]
assertThat(wasCalled[0]).isTrue()
}
@@ -165,9 +163,7 @@
assertThat(wasCalled[0]).isTrue()
wasCalled[0] = false
ViewModelProvider(object : ViewModelStoreOwnerWithCreationExtras() {
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return testFactory
- }
+ override val defaultViewModelProviderFactory = testFactory
})["customKey", ViewModel1::class.java]
assertThat(wasCalled[0]).isTrue()
}
@@ -180,9 +176,7 @@
return mStore
}
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return mFactory
- }
+ override val defaultViewModelProviderFactory = mFactory
}
open class ViewModel1 : ViewModel() {
@@ -204,15 +198,15 @@
internal open class ViewModelStoreOwnerWithCreationExtras : ViewModelStoreOwner,
HasDefaultViewModelProviderFactory {
private val viewModelStore = ViewModelStore()
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- throw UnsupportedOperationException()
- }
+ override val defaultViewModelProviderFactory: ViewModelProvider.Factory
+ get() = throw UnsupportedOperationException()
- override fun getDefaultViewModelCreationExtras(): CreationExtras {
- val extras = MutableCreationExtras()
- extras[TEST_KEY] = TEST_VALUE
- return extras
- }
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() {
+ val extras = MutableCreationExtras()
+ extras[TEST_KEY] = TEST_VALUE
+ return extras
+ }
override fun getViewModelStore(): ViewModelStore {
return viewModelStore
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 2133a87..869f652 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -123,6 +123,8 @@
method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
method public androidx.lifecycle.ViewModelStore getViewModelStore();
property public final android.os.Bundle? arguments;
+ property public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
property public final androidx.navigation.NavDestination destination;
property public final String id;
property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index 2133a87..869f652 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -123,6 +123,8 @@
method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
method public androidx.lifecycle.ViewModelStore getViewModelStore();
property public final android.os.Bundle? arguments;
+ property public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
property public final androidx.navigation.NavDestination destination;
property public final String id;
property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 2133a87..869f652 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -123,6 +123,8 @@
method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
method public androidx.lifecycle.ViewModelStore getViewModelStore();
property public final android.os.Bundle? arguments;
+ property public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
+ property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
property public final androidx.navigation.NavDestination destination;
property public final String id;
property public final androidx.lifecycle.SavedStateHandle savedStateHandle;
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
index a1fe535..3f0656e 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
@@ -221,22 +221,21 @@
return viewModelStoreProvider.getViewModelStore(id)
}
- public override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return defaultFactory
- }
+ override val defaultViewModelProviderFactory: ViewModelProvider.Factory = defaultFactory
- override fun getDefaultViewModelCreationExtras(): CreationExtras {
- val extras = MutableCreationExtras()
- (context?.applicationContext as? Application)?.let { application ->
- extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() {
+ val extras = MutableCreationExtras()
+ (context?.applicationContext as? Application)?.let { application ->
+ extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] = application
+ }
+ extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
+ extras[VIEW_MODEL_STORE_OWNER_KEY] = this
+ arguments?.let { args ->
+ extras[DEFAULT_ARGS_KEY] = args
+ }
+ return extras
}
- extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
- extras[VIEW_MODEL_STORE_OWNER_KEY] = this
- arguments?.let { args ->
- extras[DEFAULT_ARGS_KEY] = args
- }
- return extras
- }
override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryController.savedStateRegistry
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
index 07ce168f..dbaab04 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavGraphViewModelLazyTest.kt
@@ -30,7 +30,6 @@
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
@@ -279,15 +278,14 @@
return View(activity)
}
- override fun getDefaultViewModelProviderFactory(): ViewModelProvider.Factory {
- return SavedStateViewModelFactory()
- }
+ override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
- override fun getDefaultViewModelCreationExtras(): CreationExtras {
- val extras = MutableCreationExtras(super.getDefaultViewModelCreationExtras())
- extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
- return extras
- }
+ override val defaultViewModelCreationExtras: CreationExtras
+ get() {
+ val extras = MutableCreationExtras(super.defaultViewModelCreationExtras)
+ extras[DEFAULT_ARGS_KEY] = bundleOf("test" to "value")
+ return extras
+ }
}
class TestViewModel : ViewModel()
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index bcde83a..b48b6de 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -9,6 +9,14 @@
method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
method public suspend Object? prependScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? scrollTo(int index, optional androidx.paging.testing.SnapshotLoader.ScrollBehavior scrollBehavior, optional kotlin.coroutines.Continuation<kotlin.Unit>);
+ }
+
+ public enum SnapshotLoader.ScrollBehavior {
+ method public static androidx.paging.testing.SnapshotLoader.ScrollBehavior valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.paging.testing.SnapshotLoader.ScrollBehavior[] values();
+ enum_constant public static final androidx.paging.testing.SnapshotLoader.ScrollBehavior ScrollIntoPlaceholders;
+ enum_constant public static final androidx.paging.testing.SnapshotLoader.ScrollBehavior WaitForPlaceholdersToLoad;
}
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 bcde83a..b48b6de 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -9,6 +9,14 @@
method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
method public suspend Object? prependScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? scrollTo(int index, optional androidx.paging.testing.SnapshotLoader.ScrollBehavior scrollBehavior, optional kotlin.coroutines.Continuation<kotlin.Unit>);
+ }
+
+ public enum SnapshotLoader.ScrollBehavior {
+ method public static androidx.paging.testing.SnapshotLoader.ScrollBehavior valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.paging.testing.SnapshotLoader.ScrollBehavior[] values();
+ enum_constant public static final androidx.paging.testing.SnapshotLoader.ScrollBehavior ScrollIntoPlaceholders;
+ enum_constant public static final androidx.paging.testing.SnapshotLoader.ScrollBehavior WaitForPlaceholdersToLoad;
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index bcde83a..b48b6de 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -9,6 +9,14 @@
method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
method public suspend Object? prependScrollWhile(kotlin.jvm.functions.Function1<Value,java.lang.Boolean> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? scrollTo(int index, optional androidx.paging.testing.SnapshotLoader.ScrollBehavior scrollBehavior, optional kotlin.coroutines.Continuation<kotlin.Unit>);
+ }
+
+ public enum SnapshotLoader.ScrollBehavior {
+ method public static androidx.paging.testing.SnapshotLoader.ScrollBehavior valueOf(String name) throws java.lang.IllegalArgumentException;
+ method public static androidx.paging.testing.SnapshotLoader.ScrollBehavior[] values();
+ enum_constant public static final androidx.paging.testing.SnapshotLoader.ScrollBehavior ScrollIntoPlaceholders;
+ enum_constant public static final androidx.paging.testing.SnapshotLoader.ScrollBehavior WaitForPlaceholdersToLoad;
}
public final class StaticListPagingSourceFactoryKt {
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 e4f73ac..a65cc1b 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
@@ -123,6 +123,71 @@
}
/**
+ * Imitates scrolling from current index to the target index.
+ *
+ * The scroll direction (prepend or append) is dependent on current index and target index. In
+ * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
+ * index triggers [APPEND].
+ *
+ * @param [index] The target index to scroll to
+ *
+ * @param [scrollBehavior] The default scroll behavior is
+ * [ScrollBehavior.WaitForPlaceholdersToLoad]. See [ScrollBehavior] for all scroll types.
+ */
+ public suspend fun scrollTo(
+ index: Int,
+ scrollBehavior: ScrollBehavior = ScrollBehavior.WaitForPlaceholdersToLoad
+ ): @JvmSuppressWildcards Unit {
+ differ.awaitNotLoading()
+ appendOrPrependScrollTo(index, scrollBehavior)
+ differ.awaitNotLoading()
+ }
+
+ /**
+ * Scrolls from current index to targeted [index].
+ *
+ * Internally this method scrolls until it fulfills requested index
+ * differential (Math.abs(requested index - current index)) rather than scrolling
+ * to the exact requested index. This is because item indices can shift depending on scroll
+ * direction and placeholders. Therefore we try to fulfill the expected amount of scrolling
+ * rather than the actual requested index.
+ */
+ private suspend fun appendOrPrependScrollTo(
+ index: Int,
+ scrollBehavior: ScrollBehavior,
+ ) {
+ val startIndex = generations.value.lastAccessedIndex.get()
+ val loadType = if (startIndex > index) LoadType.PREPEND else LoadType.APPEND
+ when (loadType) {
+ LoadType.PREPEND -> prependScrollTo(index, startIndex, scrollBehavior)
+ LoadType.APPEND -> {
+ // TODO
+ }
+ }
+ }
+
+ private suspend fun prependScrollTo(
+ index: Int,
+ startIndex: Int,
+ scrollBehavior: ScrollBehavior
+ ) {
+ val endIndex = maxOf(0, index)
+ val scrollCount = startIndex - endIndex
+ when (scrollBehavior) {
+ ScrollBehavior.WaitForPlaceholdersToLoad -> awaitScrollTo(LoadType.PREPEND, scrollCount)
+ ScrollBehavior.ScrollIntoPlaceholders -> {
+ // TODO
+ }
+ }
+ }
+
+ private suspend fun awaitScrollTo(loadType: LoadType, scrollCount: Int) {
+ repeat(scrollCount) {
+ awaitNextItem(loadType)
+ }
+ }
+
+ /**
* Triggers load for next item, awaits for it to be loaded and returns the loaded item.
*
* It calculates the next load index based on loadType and this generation's
@@ -228,6 +293,29 @@
PREPEND,
APPEND
}
+
+ /**
+ * Determines whether the fake scroll will wait for asynchronous data to be loaded in or not.
+ *
+ * @see scrollTo
+ */
+ public enum class ScrollBehavior {
+ /**
+ * Imitates slow scrolls by waiting for item to be loaded in before triggering
+ * load on next item. A scroll with this behavior will return all available data
+ * that has been scrolled through.
+ */
+ ScrollIntoPlaceholders,
+
+ /**
+ * Imitates fast scrolling that will continue scrolling as data is being loaded in
+ * asynchronously. A scroll with this behavior will return only the data that has been
+ * loaded in by the time scrolling ends. This mode can also be used to trigger paging
+ * jumps if the number of placeholders scrolled through is larger than
+ * [PagingConfig.jumpThreshold].
+ */
+ WaitForPlaceholdersToLoad
+ }
}
internal data class Generation(
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 b65ce1a..8a4c15d 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
@@ -19,6 +19,7 @@
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
+import androidx.paging.testing.SnapshotLoader.ScrollBehavior
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -756,6 +757,172 @@
}
}
+ @Test
+ fun prependToAwait() {
+ val dataFlow = flowOf(List(100) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG,
+ initialKey = 50,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ scrollTo(42, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // initial load [50-54]
+ // prefetched [47-49], [55-57]
+ // prepended [41-46]
+ // prefetched [38-40]
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(
+ 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+ )
+ )
+ }
+ }
+
+ @Test
+ fun consecutivePrependToAwait() {
+ val dataFlow = flowOf(List(100) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG,
+ initialKey = 50,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ scrollTo(42, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // initial load [50-54]
+ // prefetched [47-49], [55-57]
+ // prepended [41-46]
+ // prefetched [38-40]
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(
+ 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57
+ )
+ )
+
+ val snapshot2 = pager.flow.asSnapshot(this) {
+ scrollTo(38, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // prefetched [35-37]
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(
+ 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+ 51, 52, 53, 54, 55, 56, 57
+ )
+ )
+ }
+ }
+
+ @Test
+ fun prependToAwait_withoutPrefetch() {
+ val dataFlow = flowOf(List(100) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 50,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ scrollTo(42, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // initial load [50-54]
+ // prepended [41-49]
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54)
+ )
+ }
+ }
+
+ @Test
+ fun prependToAwait_withoutPlaceholders() {
+ val dataFlow = flowOf(List(100) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PLACEHOLDERS,
+ initialKey = 50,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ // Without placeholders, first loaded page always starts at index[0]
+ scrollTo(0, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // initial load [50-54]
+ // prefetched [47-49], [55-57]
+ // scrollTo prepended [44-46]
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+ )
+ }
+ }
+
+ @Test
+ fun consecutivePrependToAwait_withoutPlaceholders() {
+ val dataFlow = flowOf(List(100) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PLACEHOLDERS,
+ initialKey = 50,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ // Without placeholders, first loaded page always starts at index[0]
+ scrollTo(0, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // initial load [50-54]
+ // prefetched [47-49], [55-57]
+ // scrollTo prepended [44-46]
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ // Without placeholders, first loaded page always starts at index[0]
+ scrollTo(0, ScrollBehavior.WaitForPlaceholdersToLoad)
+ }
+ // scrollTo prepended [41-43]
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+ )
+ }
+ }
+
+ @Test
+ fun prependToAwait_withoutPlaceholders_noPrefetchTriggered() {
+ val dataFlow = flowOf(List(100) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = PagingConfig(
+ pageSize = 4,
+ initialLoadSize = 8,
+ enablePlaceholders = false,
+ // a small prefetchDistance to prevent prefetch until we scroll to boundary
+ prefetchDistance = 1
+ ),
+ initialKey = 50,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ // Without placeholders, first loaded page always starts at index[0]
+ scrollTo(0)
+ }
+ // initial load [50-57]
+ // no prefetch after initial load because it didn't hit prefetch distance
+ // scrollTo prepended [46-49]
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57)
+ )
+ }
+ }
+
val CONFIG = PagingConfig(
pageSize = 3,
initialLoadSize = 5,
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
index b756b24..d655f8d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
@@ -22,8 +22,8 @@
import androidx.privacysandbox.tools.PrivacySandboxValue
import java.nio.file.Path
import kotlinx.metadata.KmClass
-import kotlinx.metadata.jvm.KotlinClassHeader
import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.Metadata
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
@@ -91,7 +91,7 @@
// ASM models annotation attributes as flat List<Objects>, so the unchecked cast is
// inevitable when some of these objects have type parameters, like the lists below.
@Suppress("UNCHECKED_CAST")
- val header = KotlinClassHeader(
+ val metadataAnnotation = Metadata(
kind = metadataValues["k"] as Int?,
metadataVersion = (metadataValues["mv"] as? List<Int>?)?.toIntArray(),
data1 = (metadataValues["d1"] as? List<String>?)?.toTypedArray(),
@@ -101,7 +101,7 @@
extraString = metadataValues["xs"] as? String?,
)
- return when (val metadata = KotlinClassMetadata.read(header)) {
+ return when (val metadata = KotlinClassMetadata.read(metadataAnnotation)) {
is KotlinClassMetadata.Class -> metadata.toKmClass()
else -> throw PrivacySandboxParsingException(
"Unable to parse Kotlin metadata from ${classNode.name}. " +
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
index 681fa6e..1427653 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
@@ -18,7 +18,8 @@
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
import javax.lang.model.type.TypeMirror
/**
@@ -28,7 +29,7 @@
env: JavacProcessingEnv,
typeMirror: TypeMirror,
nullability: XNullability?,
- override val kotlinType: KmType?
+ override val kotlinType: KmTypeContainer?
) : JavacType(
env, typeMirror, nullability
) {
@@ -45,7 +46,7 @@
constructor(
env: JavacProcessingEnv,
typeMirror: TypeMirror,
- kotlinType: KmType
+ kotlinType: KmTypeContainer
) : this(
env = env,
typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
index b9c3ce7..5d0ba49 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
@@ -21,7 +21,8 @@
import androidx.room.compiler.processing.XArrayType
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
import javax.lang.model.type.ArrayType
internal class JavacArrayType private constructor(
@@ -29,7 +30,7 @@
override val typeMirror: ArrayType,
nullability: XNullability?,
private val knownComponentNullability: XNullability?,
- override val kotlinType: KmType?
+ override val kotlinType: KmTypeContainer?
) : JavacType(
env, typeMirror, nullability
), XArrayType {
@@ -48,7 +49,7 @@
constructor(
env: JavacProcessingEnv,
typeMirror: ArrayType,
- kotlinType: KmType
+ kotlinType: KmTypeContainer
) : this(
env = env,
typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
index 135970e..e3dda74 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
@@ -20,7 +20,7 @@
import androidx.room.compiler.processing.XConstructorType
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeParameterElement
-import androidx.room.compiler.processing.javac.kotlin.KmConstructor
+import androidx.room.compiler.processing.javac.kotlin.KmConstructorContainer
import com.google.auto.common.MoreTypes
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
@@ -79,7 +79,7 @@
}
}
- override val kotlinMetadata: KmConstructor? by lazy {
+ override val kotlinMetadata: KmConstructorContainer? by lazy {
(enclosingElement as? JavacTypeElement)?.kotlinMetadata?.getConstructorMetadata(element)
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
index b760dc2..a00819f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
@@ -17,7 +17,8 @@
package androidx.room.compiler.processing.javac
import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
import javax.lang.model.type.DeclaredType
/**
@@ -29,7 +30,7 @@
env: JavacProcessingEnv,
override val typeMirror: DeclaredType,
nullability: XNullability?,
- override val kotlinType: KmType?
+ override val kotlinType: KmTypeContainer?
) : JavacType(
env, typeMirror, nullability
) {
@@ -46,7 +47,7 @@
constructor(
env: JavacProcessingEnv,
typeMirror: DeclaredType,
- kotlinType: KmType
+ kotlinType: KmTypeContainer
) : this(
env = env,
typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index b66283f..7d8bbe1 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -22,7 +22,7 @@
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XEquality
import androidx.room.compiler.processing.XHasModifiers
-import androidx.room.compiler.processing.javac.kotlin.KmElement
+import androidx.room.compiler.processing.javac.kotlin.KmFlags
import androidx.room.compiler.processing.unwrapRepeatedAnnotationsFromContainer
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreElements.isAnnotationPresent
@@ -39,7 +39,7 @@
open val element: Element
) : XElement, XEquality, InternalXAnnotated, XHasModifiers {
- abstract val kotlinMetadata: KmElement?
+ abstract val kotlinMetadata: KmFlags?
override fun <T : Annotation> getAnnotations(
annotation: KClass<T>,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
index 7c62191..20e38a3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
@@ -18,7 +18,7 @@
import androidx.room.compiler.processing.XExecutableElement
import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.javac.kotlin.KmExecutable
+import androidx.room.compiler.processing.javac.kotlin.KmFunctionContainer
import androidx.room.compiler.processing.javac.kotlin.descriptor
import javax.lang.model.element.ExecutableElement
@@ -26,7 +26,7 @@
env: JavacProcessingEnv,
override val element: ExecutableElement
) : JavacElement(env, element), XExecutableElement {
- abstract override val kotlinMetadata: KmExecutable?
+ abstract override val kotlinMetadata: KmFunctionContainer?
val descriptor by lazy {
element.descriptor()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
index a79e0fa..88f7332 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
@@ -17,8 +17,8 @@
package androidx.room.compiler.processing.javac
import androidx.room.compiler.processing.XFieldElement
-import androidx.room.compiler.processing.javac.kotlin.KmProperty
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmPropertyContainer
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
import javax.lang.model.element.VariableElement
internal class JavacFieldElement(
@@ -26,11 +26,11 @@
element: VariableElement
) : JavacVariableElement(env, element), XFieldElement {
- override val kotlinMetadata: KmProperty? by lazy {
+ override val kotlinMetadata: KmPropertyContainer? by lazy {
(enclosingElement as? JavacTypeElement)?.kotlinMetadata?.getPropertyMetadata(name)
}
- override val kotlinType: KmType?
+ override val kotlinType: KmTypeContainer?
get() = kotlinMetadata?.type
override val enclosingElement: JavacTypeElement by lazy {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
index 745a16d..3a3fcfa 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
@@ -22,7 +22,7 @@
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.XTypeParameterElement
-import androidx.room.compiler.processing.javac.kotlin.KmFunction
+import androidx.room.compiler.processing.javac.kotlin.KmFunctionContainer
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
import javax.lang.model.element.ElementKind
@@ -71,7 +71,7 @@
}
}
- override val kotlinMetadata: KmFunction? by lazy {
+ override val kotlinMetadata: KmFunctionContainer? by lazy {
(enclosingElement as? JavacTypeElement)?.kotlinMetadata?.getFunctionMetadata(element)
}
@@ -178,5 +178,5 @@
}
}
- override fun isKotlinPropertyMethod() = kotlinMetadata?.isPropertyFunction ?: false
+ override fun isKotlinPropertyMethod() = kotlinMetadata?.isPropertyFunction() ?: false
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
index ec44f2f..27aeaa5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
@@ -18,8 +18,8 @@
import androidx.room.compiler.processing.XExecutableParameterElement
import androidx.room.compiler.processing.XMemberContainer
-import androidx.room.compiler.processing.javac.kotlin.KmType
-import androidx.room.compiler.processing.javac.kotlin.KmValueParameter
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.KmValueParameterContainer
import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
import javax.lang.model.element.VariableElement
@@ -27,7 +27,7 @@
env: JavacProcessingEnv,
override val enclosingElement: JavacExecutableElement,
element: VariableElement,
- kotlinMetadataFactory: () -> KmValueParameter?,
+ kotlinMetadataFactory: () -> KmValueParameterContainer?,
val argIndex: Int
) : JavacVariableElement(env, element), XExecutableParameterElement {
@@ -38,7 +38,7 @@
argIndex = argIndex
)
- override val kotlinType: KmType?
+ override val kotlinType: KmTypeContainer?
get() = kotlinMetadata?.type
override val hasDefaultValue: Boolean
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index 2140436..cec6636 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -23,7 +23,7 @@
import androidx.room.compiler.processing.XProcessingEnvConfig
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
import com.google.auto.common.GeneratedAnnotations
import com.google.auto.common.MoreTypes
import java.util.Locale
@@ -174,7 +174,7 @@
*/
inline fun <reified T : JavacType> wrap(
typeMirror: TypeMirror,
- kotlinType: KmType?,
+ kotlinType: KmTypeContainer?,
elementNullability: XNullability?
): T {
return when (typeMirror.kind) {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index 7457b785..3dd55ea 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -21,8 +21,8 @@
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XRawType
import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.javac.kotlin.KmType
-import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
+import androidx.room.compiler.processing.javac.kotlin.KmClassContainer
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
import androidx.room.compiler.processing.ksp.ERROR_JTYPE_NAME
import androidx.room.compiler.processing.safeTypeName
import com.google.auto.common.MoreTypes
@@ -35,8 +35,8 @@
open val typeMirror: TypeMirror,
internal val maybeNullability: XNullability?,
) : XType, XEquality {
- // Kotlin type information about the type if this type is driven from kotlin code.
- abstract val kotlinType: KmType?
+ // Kotlin type information about the type if this type is driven from Kotlin code.
+ abstract val kotlinType: KmTypeContainer?
override val rawType: XRawType by lazy {
JavacRawType(env, this)
@@ -48,7 +48,7 @@
val element = MoreTypes.asTypeElement(it)
env.wrap<JavacType>(
typeMirror = it,
- kotlinType = KotlinMetadataElement.createFor(element)?.kmType,
+ kotlinType = KmClassContainer.createFor(env, element)?.type,
elementNullability = element.nullability
)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 505e75f..5b09016 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -29,7 +29,7 @@
import androidx.room.compiler.processing.collectAllMethods
import androidx.room.compiler.processing.collectFieldsIncludingPrivateSupers
import androidx.room.compiler.processing.filterMethodsByConfig
-import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
+import androidx.room.compiler.processing.javac.kotlin.KmClassContainer
import androidx.room.compiler.processing.util.MemoizedSequence
import com.google.auto.common.MoreElements
import com.google.auto.common.MoreTypes
@@ -53,7 +53,7 @@
get() = MoreElements.getPackage(element).qualifiedName.toString()
override val kotlinMetadata by lazy {
- KotlinMetadataElement.createFor(element)
+ KmClassContainer.createFor(env, element)
}
override val qualifiedName by lazy {
@@ -151,7 +151,7 @@
}
override fun findPrimaryConstructor(): JavacConstructorElement? {
- val primarySignature = kotlinMetadata?.findPrimaryConstructorSignature() ?: return null
+ val primarySignature = kotlinMetadata?.primaryConstructorSignature ?: return null
return getConstructors().firstOrNull {
primarySignature == it.descriptor
}
@@ -194,7 +194,7 @@
override val type: JavacDeclaredType by lazy {
env.wrap(
typeMirror = element.asType(),
- kotlinType = kotlinMetadata?.kmType,
+ kotlinType = kotlinMetadata?.type,
elementNullability = element.nullability
)
}
@@ -222,7 +222,7 @@
val element = MoreTypes.asTypeElement(it)
env.wrap<JavacType>(
typeMirror = it,
- kotlinType = KotlinMetadataElement.createFor(element)?.kmType,
+ kotlinType = KmClassContainer.createFor(env, element)?.type,
elementNullability = element.nullability
)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
index 50652e4..ad42e32 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
@@ -21,7 +21,7 @@
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeParameterElement
-import androidx.room.compiler.processing.javac.kotlin.KmTypeParameter
+import androidx.room.compiler.processing.javac.kotlin.KmTypeParameterContainer
import com.squareup.javapoet.TypeVariableName
import javax.lang.model.element.TypeParameterElement
@@ -29,7 +29,7 @@
env: JavacProcessingEnv,
override val enclosingElement: XElement,
override val element: TypeParameterElement,
- override val kotlinMetadata: KmTypeParameter?,
+ override val kotlinMetadata: KmTypeParameterContainer?,
) : JavacElement(env, element), XTypeParameterElement {
override val name: String
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt
index d66860f..f8c3f78 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeVariableType.kt
@@ -19,7 +19,8 @@
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeVariableType
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
+import androidx.room.compiler.processing.javac.kotlin.nullability
import com.google.auto.common.MoreTypes.asIntersection
import javax.lang.model.type.TypeKind
import javax.lang.model.type.TypeVariable
@@ -28,7 +29,7 @@
env: JavacProcessingEnv,
override val typeMirror: TypeVariable,
nullability: XNullability?,
- override val kotlinType: KmType?
+ override val kotlinType: KmTypeContainer?
) : JavacType(env, typeMirror, nullability), XTypeVariableType {
constructor(
env: JavacProcessingEnv,
@@ -43,7 +44,7 @@
constructor(
env: JavacProcessingEnv,
typeMirror: TypeVariable,
- kotlinType: KmType
+ kotlinType: KmTypeContainer
) : this(
env = env,
typeMirror = typeMirror,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
index daec9e9..58e2973 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
@@ -18,7 +18,7 @@
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XVariableElement
-import androidx.room.compiler.processing.javac.kotlin.KmType
+import androidx.room.compiler.processing.javac.kotlin.KmTypeContainer
import com.google.auto.common.MoreTypes
import javax.lang.model.element.VariableElement
@@ -27,7 +27,7 @@
override val element: VariableElement
) : JavacElement(env, element), XVariableElement {
- abstract val kotlinType: KmType?
+ abstract val kotlinType: KmTypeContainer?
override val name: String
get() = element.simpleName.toString()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/KmTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/KmTypeExt.kt
deleted file mode 100644
index a81248e..0000000
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/KmTypeExt.kt
+++ /dev/null
@@ -1,32 +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.room.compiler.processing.javac
-
-import androidx.room.compiler.processing.XNullability
-import androidx.room.compiler.processing.javac.kotlin.KmType
-
-internal val KmType.nullability: XNullability
- get() = if (isNullable()) {
- XNullability.NULLABLE
- } else {
- // if there is an upper bound information, use its nullability (e.g. it might be T : Foo?)
- if (upperBounds?.all { it.nullability == XNullability.NULLABLE } == true) {
- XNullability.NULLABLE
- } else {
- extendsBound?.nullability ?: XNullability.NONNULL
- }
- }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
index e0ccee2..fdf221a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinClassMetadataUtils.kt
@@ -16,532 +16,361 @@
package androidx.room.compiler.processing.javac.kotlin
+import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.javac.JavacProcessingEnv
import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
-import kotlinx.metadata.ClassName
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.tools.Diagnostic
import kotlinx.metadata.Flag
import kotlinx.metadata.Flags
-import kotlinx.metadata.KmAnnotation
-import kotlinx.metadata.KmClassVisitor
-import kotlinx.metadata.KmConstructorExtensionVisitor
-import kotlinx.metadata.KmConstructorVisitor
-import kotlinx.metadata.KmExtensionType
-import kotlinx.metadata.KmFunctionExtensionVisitor
-import kotlinx.metadata.KmFunctionVisitor
-import kotlinx.metadata.KmPropertyExtensionVisitor
-import kotlinx.metadata.KmPropertyVisitor
-import kotlinx.metadata.KmTypeExtensionVisitor
-import kotlinx.metadata.KmTypeParameterVisitor
-import kotlinx.metadata.KmTypeVisitor
-import kotlinx.metadata.KmValueParameterVisitor
-import kotlinx.metadata.KmVariance
-import kotlinx.metadata.jvm.JvmConstructorExtensionVisitor
-import kotlinx.metadata.jvm.JvmFieldSignature
-import kotlinx.metadata.jvm.JvmFunctionExtensionVisitor
-import kotlinx.metadata.jvm.JvmMethodSignature
-import kotlinx.metadata.jvm.JvmPropertyExtensionVisitor
-import kotlinx.metadata.jvm.JvmTypeExtensionVisitor
+import kotlinx.metadata.KmClass
+import kotlinx.metadata.KmConstructor
+import kotlinx.metadata.KmFunction
+import kotlinx.metadata.KmProperty
+import kotlinx.metadata.KmType
+import kotlinx.metadata.KmTypeParameter
+import kotlinx.metadata.KmValueParameter
import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.annotations
+import kotlinx.metadata.jvm.getterSignature
+import kotlinx.metadata.jvm.setterSignature
+import kotlinx.metadata.jvm.signature
-/** Represents the kotlin metadata for a given element. */
-internal interface KmElement {
+internal interface KmFlags {
val flags: Flags
}
-/** Represents a function or constructor. */
-internal interface KmExecutable : KmElement {
- val parameters: List<KmValueParameter>
+internal class KmClassContainer(
+ private val kmClass: KmClass
+) : KmFlags {
+ override val flags: Flags
+ get() = kmClass.flags
+
+ val type: KmTypeContainer by lazy {
+ KmTypeContainer(
+ kmType = KmType(flags),
+ typeArguments = kmClass.typeParameters.map { kmTypeParameter ->
+ KmTypeContainer(
+ kmType = KmType(kmTypeParameter.flags),
+ typeArguments = emptyList(),
+ upperBounds = kmTypeParameter.upperBounds.map { it.asContainer() }
+ )
+ }
+ )
+ }
+
+ val superType: KmTypeContainer? by lazy {
+ kmClass.supertypes.firstOrNull()?.asContainer()
+ }
+
+ val typeParameters: List<KmTypeParameterContainer> by lazy {
+ kmClass.typeParameters.map { it.asContainer() }
+ }
+
+ private val functionList: List<KmFunctionContainer> by lazy {
+ kmClass.functions.map { it.asContainer() }
+ }
+
+ private val constructorList: List<KmConstructorContainer> by lazy {
+ kmClass.constructors.map { it.asContainer(type) }
+ }
+
+ private val propertyList: List<KmPropertyContainer> by lazy {
+ kmClass.properties.map { it.asContainer() }
+ }
+
+ val primaryConstructorSignature: String? by lazy {
+ constructorList.firstOrNull { it.isPrimary() }?.descriptor
+ }
+
+ fun isObject() = Flag.Class.IS_OBJECT(flags)
+ fun isCompanionObject() = Flag.Class.IS_COMPANION_OBJECT(flags)
+ fun isAnnotationClass() = Flag.Class.IS_ANNOTATION_CLASS(flags)
+ fun isClass() = Flag.Class.IS_CLASS(flags)
+ fun isInterface() = Flag.Class.IS_INTERFACE(flags)
+ fun isDataClass() = Flag.Class.IS_DATA(flags)
+ fun isValueClass() = Flag.Class.IS_VALUE(flags)
+ fun isFunctionalInterface() = Flag.Class.IS_FUN(flags)
+ fun isExpect() = Flag.Class.IS_EXPECT(flags)
+
+ fun getFunctionMetadata(method: ExecutableElement): KmFunctionContainer? {
+ check(method.kind == ElementKind.METHOD) {
+ "must pass an element type of method"
+ }
+ val methodSignature = method.descriptor()
+ functionList.firstOrNull { it.descriptor == methodSignature }?.let {
+ return it
+ }
+ // might be a property getter or setter
+ return propertyList.firstNotNullOfOrNull { property ->
+ when {
+ property.getter?.descriptor == methodSignature -> {
+ property.getter
+ }
+
+ property.setter?.descriptor == methodSignature -> {
+ property.setter
+ }
+
+ else -> {
+ null
+ }
+ }
+ }
+ }
+
+ fun getConstructorMetadata(method: ExecutableElement): KmConstructorContainer? {
+ check(method.kind == ElementKind.CONSTRUCTOR) {
+ "must pass an element type of constructor"
+ }
+ val methodSignature = method.descriptor()
+ return constructorList.firstOrNull { it.descriptor == methodSignature }
+ }
+
+ fun getPropertyMetadata(propertyName: String): KmPropertyContainer? =
+ propertyList.firstOrNull { it.name == propertyName }
+
+ companion object {
+ /**
+ * Creates a [KmClassContainer] for the given element if it contains Kotlin metadata,
+ * otherwise this method returns null.
+ *
+ * Usually the [element] passed must represent a class. For example, if Kotlin metadata is
+ * desired for a method, then the containing class should be used as parameter.
+ */
+ fun createFor(env: JavacProcessingEnv, element: Element): KmClassContainer? {
+ val metadataAnnotation = getMetadataAnnotation(element) ?: return null
+ val classMetadata = KotlinClassMetadata.read(metadataAnnotation)
+ if (classMetadata == null) {
+ env.delegate.messager.printMessage(
+ Diagnostic.Kind.WARNING,
+ "Unable to read Kotlin metadata due to unsupported metadata version.",
+ element
+ )
+ }
+ // TODO: Support more metadata kind (file facade, synthetic class, etc...)
+ return when (classMetadata) {
+ is KotlinClassMetadata.Class -> KmClassContainer(classMetadata.toKmClass())
+ else -> {
+ env.delegate.messager.printMessage(
+ Diagnostic.Kind.WARNING,
+ "Unable to read Kotlin metadata due to unsupported metadata " +
+ "kind: $classMetadata.",
+ element
+ )
+ null
+ }
+ }
+ }
+
+ /**
+ * Search for Kotlin's Metadata annotation across the element's hierarchy.
+ */
+ private fun getMetadataAnnotation(element: Element?): Metadata? =
+ if (element != null) {
+ element.getAnnotation(Metadata::class.java)
+ ?: getMetadataAnnotation(element.enclosingElement)
+ } else {
+ null
+ }
+ }
}
-/**
- * Represents the kotlin metadata of a function
- */
-internal data class KmFunction(
- /**
- * Name of the function in byte code
- */
- val jvmName: String,
- /**
- * Name of the function in source code
- */
- val name: String,
- val descriptor: String,
- override val flags: Flags,
- val typeParameters: List<KmTypeParameter>,
- override val parameters: List<KmValueParameter>,
- val returnType: KmType,
- val receiverType: KmType?,
- val isPropertyFunction: Boolean = false
-) : KmExecutable {
+internal interface KmFunctionContainer : KmFlags {
+ /** Name of the function in source code **/
+ val name: String
+ /** Name of the function in byte code **/
+ val jvmName: String
+ val descriptor: String
+ val typeParameters: List<KmTypeParameterContainer>
+ val parameters: List<KmValueParameterContainer>
+ val returnType: KmTypeContainer
+
+ fun isPropertyFunction(): Boolean = this is KmPropertyFunctionContainerImpl
fun isSuspend() = Flag.Function.IS_SUSPEND(flags)
- fun isExtension() = receiverType != null
+ fun isExtension() =
+ this is KmFunctionContainerImpl && this.kmFunction.receiverParameterType != null
}
-/**
- * Represents the kotlin metadata of a constructor
- */
-internal data class KmConstructor(
- val descriptor: String,
+private class KmFunctionContainerImpl(
+ val kmFunction: KmFunction,
+ override val returnType: KmTypeContainer,
+) : KmFunctionContainer {
+ override val flags: Flags
+ get() = kmFunction.flags
+ override val name: String
+ get() = kmFunction.name
+ override val jvmName: String
+ get() = kmFunction.signature!!.name
+ override val descriptor: String
+ get() = kmFunction.signature!!.asString()
+ override val typeParameters: List<KmTypeParameterContainer>
+ get() = kmFunction.typeParameters.map { it.asContainer() }
+ override val parameters: List<KmValueParameterContainer>
+ get() = kmFunction.valueParameters.map { it.asContainer() }
+}
+
+private class KmPropertyFunctionContainerImpl(
override val flags: Flags,
- override val parameters: List<KmValueParameter>
-) : KmExecutable {
+ override val name: String,
+ override val jvmName: String,
+ override val descriptor: String,
+ override val parameters: List<KmValueParameterContainer>,
+ override val returnType: KmTypeContainer,
+) : KmFunctionContainer {
+ override val typeParameters: List<KmTypeParameterContainer> = emptyList()
+}
+
+internal class KmConstructorContainer(
+ private val kmConstructor: KmConstructor,
+ override val returnType: KmTypeContainer
+) : KmFunctionContainer {
+ override val flags: Flags
+ get() = kmConstructor.flags
+ override val name: String = "<init>"
+ override val jvmName: String = name
+ override val descriptor: String
+ get() = checkNotNull(kmConstructor.signature).asString()
+ override val typeParameters: List<KmTypeParameterContainer> = emptyList()
+ override val parameters: List<KmValueParameterContainer> by lazy {
+ kmConstructor.valueParameters.map { it.asContainer() }
+ }
fun isPrimary() = !Flag.Constructor.IS_SECONDARY(flags)
}
-internal data class KmProperty(
- val name: String,
- override val flags: Flags,
- val type: KmType,
- val getter: KmFunction?,
- val setter: KmFunction?
-) : KmElement {
- val typeParameters
- get() = type.typeArguments
-
- fun isNullable() = Flag.Type.IS_NULLABLE(type.flags)
-}
-
-internal data class KmType(
- override val flags: Flags,
- val typeArguments: List<KmType>,
- /** The extends bounds are only non-null for wildcard (i.e. in/out variant) types. */
- val extendsBound: KmType? = null,
- /** The upper bounds are only non-null for type variable types with upper bounds. */
- val upperBounds: List<KmType>? = null,
- val isExtensionType: Boolean
-) : KmElement {
- fun isNullable() = Flag.Type.IS_NULLABLE(flags)
- fun erasure(): KmType = KmType(
- flags,
- emptyList(),
- extendsBound?.erasure(),
- // The erasure of a type variable is equal to the erasure of the first upper bound.
- upperBounds?.firstOrNull()?.erasure()?.let { listOf(it) },
- isExtensionType
- )
-}
-
-internal data class KmTypeParameter(
- val name: String,
- override val flags: Flags,
- val upperBounds: List<KmType>
-) : KmElement {
- fun asKmType() = KmType(
- flags = flags,
- typeArguments = emptyList(),
- upperBounds = upperBounds,
- isExtensionType = false
- )
-}
-
-/**
- * Represents the kotlin metadata of a parameter
- */
-internal data class KmValueParameter(
- val name: String,
- val type: KmType,
+internal class KmPropertyContainer(
+ private val kmProperty: KmProperty,
+ val type: KmTypeContainer,
+ val getter: KmFunctionContainer?,
+ val setter: KmFunctionContainer?
+) : KmFlags {
override val flags: Flags
-) : KmElement {
+ get() = kmProperty.flags
+ val name: String
+ get() = kmProperty.name
+ val typeParameters: List<KmTypeContainer>
+ get() = type.typeArguments
+ fun isNullable() = type.isNullable()
+}
+
+internal class KmTypeContainer(
+ private val kmType: KmType,
+ val typeArguments: List<KmTypeContainer>,
+ /** The extends bounds are only non-null for wildcard (i.e. in/out variant) types. */
+ val extendsBound: KmTypeContainer? = null,
+ /** The upper bounds are only non-null for type variable types with upper bounds. */
+ val upperBounds: List<KmTypeContainer>? = null
+) : KmFlags {
+ override val flags: Flags
+ get() = kmType.flags
+ fun isExtensionType() =
+ kmType.annotations.any { it.className == "kotlin/ExtensionFunctionType" }
+ fun isNullable() = Flag.Type.IS_NULLABLE(flags)
+
+ fun erasure(): KmTypeContainer = KmTypeContainer(
+ kmType = kmType,
+ typeArguments = emptyList(),
+ extendsBound = extendsBound?.erasure(),
+ // The erasure of a type variable is equal to the erasure of the first upper bound.
+ upperBounds = upperBounds?.firstOrNull()?.erasure()?.let { listOf(it) },
+ )
+}
+
+internal val KmTypeContainer.nullability: XNullability
+ get() = if (isNullable()) {
+ XNullability.NULLABLE
+ } else {
+ // if there is an upper bound information, use its nullability (e.g. it might be T : Foo?)
+ if (upperBounds?.all { it.nullability == XNullability.NULLABLE } == true) {
+ XNullability.NULLABLE
+ } else {
+ extendsBound?.nullability ?: XNullability.NONNULL
+ }
+ }
+
+internal class KmTypeParameterContainer(
+ private val kmTypeParameter: KmTypeParameter,
+ val upperBounds: List<KmTypeContainer>
+) : KmFlags {
+ override val flags: Flags
+ get() = kmTypeParameter.flags
+ val name: String
+ get() = kmTypeParameter.name
+}
+
+internal class KmValueParameterContainer(
+ private val kmValueParameter: KmValueParameter,
+ val type: KmTypeContainer
+) : KmFlags {
+ override val flags: Flags
+ get() = kmValueParameter.flags
+ val name: String
+ get() = kmValueParameter.name
fun isNullable() = type.isNullable()
fun hasDefault() = Flag.ValueParameter.DECLARES_DEFAULT_VALUE(flags)
}
-internal data class KmClassTypeInfo(
- val kmType: KmType,
- val superType: KmType?,
- val typeParameters: List<KmTypeParameter>
-)
+private fun KmFunction.asContainer(): KmFunctionContainer =
+ KmFunctionContainerImpl(
+ kmFunction = this,
+ returnType = this.returnType.asContainer()
+ )
-internal fun KotlinClassMetadata.Class.readFunctions(): List<KmFunction> =
- mutableListOf<KmFunction>().apply { accept(FunctionReader(this)) }
+private fun KmConstructor.asContainer(returnType: KmTypeContainer): KmConstructorContainer =
+ KmConstructorContainer(
+ kmConstructor = this,
+ returnType = returnType
+ )
-private class FunctionReader(val result: MutableList<KmFunction>) : KmClassVisitor() {
- override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor {
- return object : KmFunctionVisitor() {
-
- lateinit var methodSignature: JvmMethodSignature
- private val typeParameters = mutableListOf<KmTypeParameter>()
- val parameters = mutableListOf<KmValueParameter>()
- lateinit var returnType: KmType
- var receiverType: KmType? = null
-
- override fun visitTypeParameter(
- flags: Flags,
- name: String,
- id: Int,
- variance: KmVariance
- ): KmTypeParameterVisitor {
- return TypeParameterReader(name, flags) {
- typeParameters.add(it)
- }
- }
-
- override fun visitValueParameter(
- flags: Flags,
- name: String
- ): KmValueParameterVisitor {
- return ValueParameterReader(name, flags) {
- parameters.add(it)
- }
- }
-
- override fun visitReceiverParameterType(flags: Flags): KmTypeVisitor? {
- return TypeReader(flags) {
- receiverType = it
- }
- }
-
- override fun visitExtensions(type: KmExtensionType): KmFunctionExtensionVisitor {
- if (type != JvmFunctionExtensionVisitor.TYPE) {
- error("Unsupported extension type: $type")
- }
- return object : JvmFunctionExtensionVisitor() {
- override fun visit(signature: JvmMethodSignature?) {
- methodSignature = signature!!
- }
- }
- }
-
- override fun visitReturnType(flags: Flags): KmTypeVisitor {
- return TypeReader(flags) {
- returnType = it
- }
- }
-
- override fun visitEnd() {
- result.add(
- KmFunction(
- name = name,
- jvmName = methodSignature.name,
- descriptor = methodSignature.asString(),
- flags = flags,
- typeParameters = typeParameters,
- parameters = parameters,
- returnType = returnType,
- receiverType = receiverType
- )
- )
- }
- }
- }
-}
-
-internal fun KotlinClassMetadata.Class.readConstructors(): List<KmConstructor> =
- mutableListOf<KmConstructor>().apply { accept(ConstructorReader(this)) }
-
-private class ConstructorReader(val result: MutableList<KmConstructor>) : KmClassVisitor() {
- override fun visitConstructor(flags: Flags): KmConstructorVisitor {
- return object : KmConstructorVisitor() {
-
- lateinit var descriptor: String
- val parameters = mutableListOf<KmValueParameter>()
-
- override fun visitValueParameter(
- flags: Flags,
- name: String
- ): KmValueParameterVisitor {
- return ValueParameterReader(name, flags) {
- parameters.add(it)
- }
- }
-
- override fun visitExtensions(type: KmExtensionType): KmConstructorExtensionVisitor {
- if (type != JvmConstructorExtensionVisitor.TYPE) {
- error("Unsupported extension type: $type")
- }
- return object : JvmConstructorExtensionVisitor() {
- override fun visit(signature: JvmMethodSignature?) {
- descriptor = signature!!.asString()
- }
- }
- }
-
- override fun visitEnd() {
- result.add(KmConstructor(descriptor, flags, parameters))
- }
- }
- }
-}
-
-internal class KotlinMetadataClassFlags(val classMetadata: KotlinClassMetadata.Class) {
-
- private val flags: Flags by lazy {
- var theFlags: Flags = 0
- classMetadata.accept(object : KmClassVisitor() {
- override fun visit(flags: Flags, name: ClassName) {
- theFlags = flags
- super.visit(flags, name)
- }
- })
- return@lazy theFlags
- }
-
- fun isObject(): Boolean = Flag.Class.IS_OBJECT(flags)
-
- fun isCompanionObject(): Boolean = Flag.Class.IS_COMPANION_OBJECT(flags)
-
- fun isAnnotationClass(): Boolean = Flag.Class.IS_ANNOTATION_CLASS(flags)
-
- fun isInterface(): Boolean = Flag.Class.IS_INTERFACE(flags)
-
- fun isClass(): Boolean = Flag.Class.IS_CLASS(flags)
-
- fun isDataClass(): Boolean = Flag.Class.IS_DATA(flags)
-
- fun isValueClass(): Boolean = Flag.Class.IS_VALUE(flags)
-
- fun isFunctionalInterface(): Boolean = Flag.Class.IS_FUN(flags)
-
- fun isExpect(): Boolean = Flag.Class.IS_EXPECT(flags)
-}
-
-internal fun KotlinClassMetadata.Class.readProperties(): List<KmProperty> =
- mutableListOf<KmProperty>().apply { accept(PropertyReader(this)) }
-
-/**
- * Reads the properties of a class declaration
- */
-private class PropertyReader(
- val result: MutableList<KmProperty>
-) : KmClassVisitor() {
- override fun visitProperty(
- flags: Flags,
- name: String,
- getterFlags: Flags,
- setterFlags: Flags
- ): KmPropertyVisitor {
- var setterParam: KmValueParameter? = null
- var getter: JvmMethodSignature? = null
- var setter: JvmMethodSignature? = null
- return object : KmPropertyVisitor() {
- lateinit var returnType: KmType
- override fun visitEnd() {
- result.add(
- KmProperty(
- type = returnType,
- name = name,
- flags = flags,
- setter = setter?.let { setterSignature ->
- // setter parameter visitor may not be invoked when not declared
- // explicitly
- val param = setterParam ?: KmValueParameter(
- // kotlinc will set this to set-? but it is better to not expose
- // it here since it is not valid name
- name = "set-?".sanitizeAsJavaParameterName(0),
- type = returnType,
- flags = 0
- )
- KmFunction(
- jvmName = setterSignature.name,
- name = JvmAbi.computeSetterName(name),
- descriptor = setterSignature.asString(),
- flags = setterFlags,
- typeParameters = emptyList(),
- parameters = listOf(param),
- returnType = KM_VOID_TYPE,
- receiverType = null,
- isPropertyFunction = true
- )
- },
- getter = getter?.let { getterSignature ->
- KmFunction(
- jvmName = getterSignature.name,
- name = JvmAbi.computeGetterName(name),
- descriptor = getterSignature.asString(),
- flags = getterFlags,
- typeParameters = emptyList(),
- parameters = emptyList(),
- returnType = returnType,
- receiverType = null,
- isPropertyFunction = true
- )
- }
- )
- )
- }
-
- override fun visitReturnType(flags: Flags): KmTypeVisitor {
- return TypeReader(flags) {
- returnType = it
- }
- }
-
- override fun visitSetterParameter(
- flags: Flags,
- name: String
- ): KmValueParameterVisitor {
- return ValueParameterReader(
- name = name,
- flags = flags
- ) {
- setterParam = it
- }
- }
-
- override fun visitExtensions(type: KmExtensionType): KmPropertyExtensionVisitor? {
- if (type != JvmPropertyExtensionVisitor.TYPE) {
- return null
- }
- return object : JvmPropertyExtensionVisitor() {
- override fun visit(
- jvmFlags: Flags,
- fieldSignature: JvmFieldSignature?,
- getterSignature: JvmMethodSignature?,
- setterSignature: JvmMethodSignature?
- ) {
- getter = getterSignature
- setter = setterSignature
- }
- }
- }
- }
- }
-}
-
-/**
- * Reads a type description and calls the output with the read value
- */
-private class TypeReader(
- private val flags: Flags,
- private val output: (KmType) -> Unit
-) : KmTypeVisitor() {
- private val typeArguments = mutableListOf<KmType>()
- private var extendsBound: KmType? = null
- private var isExtensionType = false
- override fun visitArgument(flags: Flags, variance: KmVariance): KmTypeVisitor {
- return TypeReader(flags) {
- typeArguments.add(it)
- }
- }
-
- override fun visitFlexibleTypeUpperBound(
- flags: Flags,
- typeFlexibilityId: String?
- ): KmTypeVisitor {
- return TypeReader(flags) {
- extendsBound = it
- }
- }
-
- override fun visitExtensions(type: KmExtensionType): KmTypeExtensionVisitor? {
- if (type != JvmTypeExtensionVisitor.TYPE) return null
- return object : JvmTypeExtensionVisitor() {
- override fun visitAnnotation(annotation: KmAnnotation) {
- if (annotation.className == "kotlin/ExtensionFunctionType") {
- isExtensionType = true
- }
- }
- }
- }
-
- override fun visitEnd() {
- output(
- KmType(
- flags = flags,
- typeArguments = typeArguments,
- extendsBound = extendsBound,
- isExtensionType = isExtensionType
+private fun KmProperty.asContainer(): KmPropertyContainer =
+ KmPropertyContainer(
+ kmProperty = this,
+ type = this.returnType.asContainer(),
+ getter = getterSignature?.let {
+ KmPropertyFunctionContainerImpl(
+ flags = this.getterFlags,
+ name = JvmAbi.computeGetterName(this.name),
+ jvmName = it.name,
+ descriptor = it.asString(),
+ parameters = emptyList(),
+ returnType = this.returnType.asContainer(),
)
- )
- }
-}
-
-/**
- * Reads the value parameter of a function or constructor and calls the output with the read value
- */
-private class ValueParameterReader(
- val name: String,
- val flags: Flags,
- val output: (KmValueParameter) -> Unit
-) : KmValueParameterVisitor() {
- lateinit var type: KmType
- override fun visitType(flags: Flags): KmTypeVisitor {
- return TypeReader(flags) {
- type = it
- }
- }
-
- override fun visitEnd() {
- output(
- KmValueParameter(
- name = name,
- type = type,
- flags = flags
+ },
+ setter = setterSignature?.let {
+ // setter parameter visitor may not be available when not declared explicitly
+ val param = this.setterParameter ?: KmValueParameter(
+ flags = 0,
+ // kotlinc will set this to set-? but it is better to not expose
+ // it here since it is not valid name
+ name = "set-?".sanitizeAsJavaParameterName(0)
+ ).apply { type = [email protected] }
+ KmPropertyFunctionContainerImpl(
+ flags = this.setterFlags,
+ name = JvmAbi.computeSetterName(this.name),
+ jvmName = it.name,
+ descriptor = it.asString(),
+ parameters = listOf(param.asContainer()),
+ returnType = KmType(0).asContainer(),
)
- )
- }
-}
+ },
+ )
-/**
- * Reads a class declaration and turns it into a KmType for both itself and its super type
- */
-internal class ClassAsKmTypeReader(
- val output: (KmClassTypeInfo) -> Unit
-) : KmClassVisitor() {
- private var flags: Flags = 0
- private val typeParameters = mutableListOf<KmTypeParameter>()
- private var superType: KmType? = null
- override fun visit(flags: Flags, name: ClassName) {
- this.flags = flags
- }
+private fun KmType.asContainer(): KmTypeContainer =
+ KmTypeContainer(
+ kmType = this,
+ typeArguments = this.arguments.mapNotNull { it.type?.asContainer() }
+ )
- override fun visitTypeParameter(
- flags: Flags,
- name: String,
- id: Int,
- variance: KmVariance
- ): KmTypeParameterVisitor {
- return TypeParameterReader(name, flags) {
- typeParameters.add(it)
- }
- }
+private fun KmTypeParameter.asContainer(): KmTypeParameterContainer =
+ KmTypeParameterContainer(
+ kmTypeParameter = this,
+ upperBounds = this.upperBounds.map { it.asContainer() }
+ )
- override fun visitSupertype(flags: Flags): KmTypeVisitor {
- return TypeReader(flags) {
- superType = it
- }
- }
-
- override fun visitEnd() {
- output(
- KmClassTypeInfo(
- kmType = KmType(
- flags = flags,
- typeArguments = typeParameters.map(KmTypeParameter::asKmType),
- isExtensionType = false
- ),
- typeParameters = typeParameters,
- superType = superType
- )
- )
- }
-}
-
-private class TypeParameterReader(
- private val name: String,
- private val flags: Flags,
- private val output: (KmTypeParameter) -> Unit
-) : KmTypeParameterVisitor() {
- private var upperBounds: MutableList<KmType> = mutableListOf()
- override fun visitEnd() {
- output(
- KmTypeParameter(
- name = name,
- flags = flags,
- upperBounds = upperBounds
- )
- )
- }
-
- override fun visitUpperBound(flags: Flags): KmTypeVisitor {
- return TypeReader(flags) {
- upperBounds.add(it)
- }
- }
-}
-
-private val KM_VOID_TYPE = KmType(
- flags = 0,
- typeArguments = emptyList(),
- extendsBound = null,
- isExtensionType = false
-)
\ No newline at end of file
+private fun KmValueParameter.asContainer(): KmValueParameterContainer =
+ KmValueParameterContainer(
+ kmValueParameter = this,
+ type = this.type.asContainer()
+ )
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
deleted file mode 100644
index 2d32a98..0000000
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElement.kt
+++ /dev/null
@@ -1,150 +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.room.compiler.processing.javac.kotlin
-
-import kotlinx.metadata.jvm.KotlinClassHeader
-import kotlinx.metadata.jvm.KotlinClassMetadata
-import javax.lang.model.element.Element
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.ExecutableElement
-
-/**
- * Utility class for processors that wants to run kotlin specific code.
- */
-internal class KotlinMetadataElement(
- val element: Element,
- private val classMetadata: KotlinClassMetadata.Class
-) : KmElement {
- private val typeInfo: KmClassTypeInfo by lazy {
- lateinit var result: KmClassTypeInfo
- classMetadata.accept(
- ClassAsKmTypeReader {
- result = it
- }
- )
- result
- }
- val kmType
- get() = typeInfo.kmType
- override val flags
- get() = kmType.flags
- val superType
- get() = typeInfo.superType
- val typeParameters
- get() = typeInfo.typeParameters
- private val functionList: List<KmFunction> by lazy { classMetadata.readFunctions() }
- private val constructorList: List<KmConstructor> by lazy { classMetadata.readConstructors() }
- private val propertyList: List<KmProperty> by lazy { classMetadata.readProperties() }
- private val classFlags: KotlinMetadataClassFlags by lazy {
- KotlinMetadataClassFlags(classMetadata)
- }
-
- private val ExecutableElement.descriptor: String
- get() = descriptor()
-
- fun findPrimaryConstructorSignature() = constructorList.firstOrNull {
- it.isPrimary()
- }?.descriptor
-
- fun isObject(): Boolean = classFlags.isObject()
- fun isCompanionObject(): Boolean = classFlags.isCompanionObject()
- fun isAnnotationClass(): Boolean = classFlags.isAnnotationClass()
- fun isClass(): Boolean = classFlags.isClass()
- fun isInterface(): Boolean = classFlags.isInterface()
- fun isDataClass(): Boolean = classFlags.isDataClass()
- fun isValueClass(): Boolean = classFlags.isValueClass()
- fun isFunctionalInterface(): Boolean = classFlags.isFunctionalInterface()
- fun isExpect(): Boolean = classFlags.isExpect()
-
- fun getFunctionMetadata(method: ExecutableElement): KmFunction? {
- check(method.kind == ElementKind.METHOD) {
- "must pass an element type of method"
- }
- val methodSignature = method.descriptor
- functionList.firstOrNull { it.descriptor == methodSignature }?.let {
- return it
- }
- // might be a property getter or setter
- return propertyList.firstNotNullOfOrNull { property ->
- when {
- property.getter?.descriptor == methodSignature -> {
- property.getter
- }
- property.setter?.descriptor == methodSignature -> {
- property.setter
- }
- else -> {
- null
- }
- }
- }
- }
-
- fun getConstructorMetadata(method: ExecutableElement): KmConstructor? {
- check(method.kind == ElementKind.CONSTRUCTOR) {
- "must pass an element type of constructor"
- }
- val methodSignature = method.descriptor
- return constructorList.firstOrNull { it.descriptor == methodSignature }
- }
-
- fun getPropertyMetadata(propertyName: String) = propertyList.firstOrNull {
- it.name == propertyName
- }
-
- companion object {
- /**
- * Creates a [KotlinMetadataElement] for the given element if it contains Kotlin metadata,
- * otherwise this method returns null.
- *
- * Usually the [element] passed must represent a class. For example, if kotlin metadata is
- * desired for a method, then the containing class should be used as parameter.
- */
- fun createFor(element: Element): KotlinMetadataElement? {
- val metadata = getMetadataAnnotation(element)?.run {
- KotlinClassHeader(
- kind = kind,
- metadataVersion = metadataVersion,
- data1 = data1,
- data2 = data2,
- extraString = extraString,
- packageName = packageName,
- extraInt = extraInt
- ).let {
- // TODO: Support more metadata kind (file facade, synthetic class, etc...)
- KotlinClassMetadata.read(it) as? KotlinClassMetadata.Class
- }
- }
- return if (metadata != null) {
- KotlinMetadataElement(element, metadata)
- } else {
- null
- }
- }
-
- /**
- * Search for Kotlin's Metadata annotation across the element's hierarchy.
- */
- private fun getMetadataAnnotation(element: Element?): Metadata? =
- if (element != null) {
- element.getAnnotation(Metadata::class.java)
- ?: getMetadataAnnotation(element.enclosingElement)
- } else {
- null
- }
- }
-}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
index 0d99b43..5915327 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
@@ -17,8 +17,8 @@
package androidx.room.compiler.processing.javac.kotlin
import androidx.room.compiler.processing.XNullability
+import androidx.room.compiler.processing.XProcessingEnvConfig
import androidx.room.compiler.processing.javac.JavacProcessingEnv
-import androidx.room.compiler.processing.javac.nullability
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.runJavaProcessorTest
@@ -26,14 +26,14 @@
import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
-import org.junit.AssumptionViolatedException
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.util.ElementFilter
+import org.junit.AssumptionViolatedException
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class KotlinMetadataElementTest(
@@ -164,7 +164,7 @@
assertThat(
testClassElement.getConstructors().map {
val desc = it.descriptor()
- desc to (desc == metadataElement.findPrimaryConstructorSignature())
+ desc to (desc == metadataElement.primaryConstructorSignature)
}
).containsExactly(
"<init>(Ljava/lang/String;)V" to true,
@@ -347,7 +347,7 @@
)
fun assertSetter(
- kmFunction: KmFunction?,
+ kmFunction: KmFunctionContainer?,
name: String,
jvmName: String,
paramNullable: Boolean
@@ -366,7 +366,7 @@
}
fun assertSetter(
- metadata: KotlinMetadataElement,
+ metadata: KmClassContainer,
method: ExecutableElement,
name: String,
jvmName: String,
@@ -391,7 +391,7 @@
}
fun assertGetter(
- kmFunction: KmFunction?,
+ kmFunction: KmFunctionContainer?,
name: String,
jvmName: String,
returnsNullable: Boolean
@@ -582,7 +582,7 @@
invocation,
"Subject"
)
- fun assertParams(params: List<KmValueParameter>?) {
+ fun assertParams(params: List<KmValueParameterContainer>?) {
assertThat(
params?.map {
Triple(
@@ -670,22 +670,22 @@
)
simpleRun(listOf(src)) { invocation ->
val (_, simple) = getMetadataElement(invocation, "Simple")
- assertThat(simple.kmType.isNullable()).isFalse()
- assertThat(simple.kmType.typeArguments).isEmpty()
+ assertThat(simple.type.isNullable()).isFalse()
+ assertThat(simple.type.typeArguments).isEmpty()
val (_, twoArgGeneric) = getMetadataElement(invocation, "TwoArgGeneric")
- assertThat(twoArgGeneric.kmType.isNullable()).isFalse()
- assertThat(twoArgGeneric.kmType.typeArguments).hasSize(2)
- assertThat(twoArgGeneric.kmType.typeArguments[0].isNullable()).isFalse()
- assertThat(twoArgGeneric.kmType.typeArguments[1].isNullable()).isFalse()
+ assertThat(twoArgGeneric.type.isNullable()).isFalse()
+ assertThat(twoArgGeneric.type.typeArguments).hasSize(2)
+ assertThat(twoArgGeneric.type.typeArguments[0].isNullable()).isFalse()
+ assertThat(twoArgGeneric.type.typeArguments[1].isNullable()).isFalse()
val (_, withUpperBounds) = getMetadataElement(invocation, "WithUpperBounds")
- assertThat(withUpperBounds.kmType.typeArguments).hasSize(2)
- assertThat(withUpperBounds.kmType.typeArguments[0].upperBounds).hasSize(1)
- assertThat(withUpperBounds.kmType.typeArguments[0].upperBounds!![0].isNullable())
+ assertThat(withUpperBounds.type.typeArguments).hasSize(2)
+ assertThat(withUpperBounds.type.typeArguments[0].upperBounds).hasSize(1)
+ assertThat(withUpperBounds.type.typeArguments[0].upperBounds!![0].isNullable())
.isFalse()
- assertThat(withUpperBounds.kmType.typeArguments[1].upperBounds).hasSize(1)
- assertThat(withUpperBounds.kmType.typeArguments[1].upperBounds!![0].isNullable())
+ assertThat(withUpperBounds.type.typeArguments[1].upperBounds).hasSize(1)
+ assertThat(withUpperBounds.type.typeArguments[1].upperBounds!![0].isNullable())
.isTrue()
val (_, withSuperType) = getMetadataElement(invocation, "WithSuperType")
@@ -821,7 +821,8 @@
private fun getMetadataElement(processingEnv: ProcessingEnvironment, qName: String) =
processingEnv.elementUtils.getTypeElement(qName).let {
- it to KotlinMetadataElement.createFor(it)!!
+ it to KmClassContainer.createFor(
+ JavacProcessingEnv(processingEnv, XProcessingEnvConfig.DEFAULT), it)!!
}
companion object {
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
index 25932b8..3113ac0 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
@@ -16,6 +16,16 @@
package com.example.androidx.mediarouting;
+import static com.example.androidx.mediarouting.data.RouteItem.ControlFilter.BASIC;
+import static com.example.androidx.mediarouting.data.RouteItem.DeviceType.SPEAKER;
+import static com.example.androidx.mediarouting.data.RouteItem.DeviceType.TV;
+import static com.example.androidx.mediarouting.data.RouteItem.PlaybackStream.MUSIC;
+import static com.example.androidx.mediarouting.data.RouteItem.PlaybackType.REMOTE;
+import static com.example.androidx.mediarouting.data.RouteItem.VolumeHandling.VARIABLE;
+
+import android.content.Context;
+import android.content.res.Resources;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -29,21 +39,29 @@
/** Holds the data needed to control the provider for the routes dynamically. */
public final class RoutesManager {
+ private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
+ private static final int VOLUME_MAX = 25;
+ private static final int VOLUME_DEFAULT = 5;
+
private boolean mDynamicRoutingEnabled;
+
+ private Context mContext;
private final Map<String, RouteItem> mRouteItems;
private static RoutesManager sInstance;
- private RoutesManager() {
- this.mDynamicRoutingEnabled = true;
- this.mRouteItems = new HashMap<>();
+ private RoutesManager(Context context) {
+ mContext = context;
+ mDynamicRoutingEnabled = true;
+ mRouteItems = new HashMap<>();
+ initTestRoutes();
}
/** Singleton method. */
@NonNull
- public static RoutesManager getInstance() {
+ public static RoutesManager getInstance(@NonNull Context context) {
synchronized (RoutesManager.class) {
if (sInstance == null) {
- sInstance = new RoutesManager();
+ sInstance = new RoutesManager(context);
}
}
return sInstance;
@@ -86,4 +104,65 @@
public void addOrUpdateRoute(@NonNull RouteItem routeItem) {
mRouteItems.put(routeItem.getId(), routeItem);
}
+
+ private void initTestRoutes() {
+ Resources r = mContext.getResources();
+
+ RouteItem r1 = new RouteItem();
+ r1.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "1");
+ r1.setName(r.getString(R.string.dg_tv_route_name1));
+ r1.setDescription(r.getString(R.string.sample_route_description));
+ r1.setControlFilter(BASIC);
+ r1.setDeviceType(TV);
+ r1.setPlaybackStream(MUSIC);
+ r1.setPlaybackType(REMOTE);
+ r1.setVolumeHandling(VARIABLE);
+ r1.setVolumeMax(VOLUME_MAX);
+ r1.setVolume(VOLUME_DEFAULT);
+ r1.setCanDisconnect(true);
+
+ RouteItem r2 = new RouteItem();
+ r2.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "2");
+ r2.setName(r.getString(R.string.dg_tv_route_name2));
+ r2.setDescription(r.getString(R.string.sample_route_description));
+ r2.setControlFilter(BASIC);
+ r2.setDeviceType(TV);
+ r2.setPlaybackStream(MUSIC);
+ r2.setPlaybackType(REMOTE);
+ r2.setVolumeHandling(VARIABLE);
+ r2.setVolumeMax(VOLUME_MAX);
+ r2.setVolume(VOLUME_DEFAULT);
+ r2.setCanDisconnect(true);
+
+ RouteItem r3 = new RouteItem();
+ r3.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "3");
+ r3.setName(r.getString(R.string.dg_speaker_route_name3));
+ r3.setDescription(r.getString(R.string.sample_route_description));
+ r3.setControlFilter(BASIC);
+ r3.setDeviceType(SPEAKER);
+ r3.setPlaybackStream(MUSIC);
+ r3.setPlaybackType(REMOTE);
+ r3.setVolumeHandling(VARIABLE);
+ r3.setVolumeMax(VOLUME_MAX);
+ r3.setVolume(VOLUME_DEFAULT);
+ r3.setCanDisconnect(true);
+
+ RouteItem r4 = new RouteItem();
+ r4.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "4");
+ r4.setName(r.getString(R.string.dg_speaker_route_name4));
+ r4.setDescription(r.getString(R.string.sample_route_description));
+ r4.setControlFilter(BASIC);
+ r4.setDeviceType(SPEAKER);
+ r4.setPlaybackStream(MUSIC);
+ r4.setPlaybackType(REMOTE);
+ r4.setVolumeHandling(VARIABLE);
+ r4.setVolumeMax(VOLUME_MAX);
+ r4.setVolume(VOLUME_DEFAULT);
+ r4.setCanDisconnect(true);
+
+ mRouteItems.put(r1.getId(), r1);
+ mRouteItems.put(r2.getId(), r2);
+ mRouteItems.put(r3.getId(), r3);
+ mRouteItems.put(r4.getId(), r4);
+ }
}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
index 9c40caa..4acd47a 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
@@ -62,7 +62,7 @@
mMediaRouter = MediaRouter.getInstance(this);
- mRoutesManager = RoutesManager.getInstance();
+ mRoutesManager = RoutesManager.getInstance(getApplicationContext());
mConnection = new ProviderServiceConnection();
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
index baf3cfec..2e4a3a5 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
@@ -135,7 +135,8 @@
/** Reload the isDynamicRouteEnabled flag. */
public void reloadDynamicRoutesEnabled() {
- boolean isDynamicRoutesEnabled = RoutesManager.getInstance().isDynamicRoutingEnabled();
+ boolean isDynamicRoutesEnabled = RoutesManager.getInstance(getContext())
+ .isDynamicRoutingEnabled();
MediaRouteProviderDescriptor providerDescriptor =
new MediaRouteProviderDescriptor.Builder(getDescriptor())
.setSupportsDynamicGroupRoute(isDynamicRoutesEnabled)
@@ -154,7 +155,7 @@
IntentSender is = PendingIntent.getActivity(getContext(), 99, settingsIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE).getIntentSender();
- List<RouteItem> routeItems = RoutesManager.getInstance().getRouteItems();
+ List<RouteItem> routeItems = RoutesManager.getInstance(getContext()).getRouteItems();
for (RouteItem routeItem : routeItems) {
MediaRouteDescriptor routeDescriptor = buildRouteDescriptor(routeItem, is);
mVolumes.put(routeItem.getId(), routeItem.getVolume());
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
index 1b34fd8..44d7542 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
@@ -29,6 +29,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.Tab
import androidx.tv.material3.TabRow
@@ -66,6 +67,7 @@
/**
* Pill indicator tab row for reference
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun PillIndicatorTabRow(
tabs: List<String>,
diff --git a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
index b2c506e..126c1e8 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
@@ -45,6 +46,7 @@
/**
* Tab row with a Pill indicator
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun PillIndicatorTabRow() {
@@ -74,6 +76,7 @@
/**
* Tab row with an Underlined indicator
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun UnderlinedIndicatorTabRow() {
@@ -109,6 +112,7 @@
/**
* Tab row with delay between tab changes
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun TabRowWithDebounce() {
@@ -147,6 +151,7 @@
/**
* Tab changes onClick instead of onFocus
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun OnClickNavigation() {
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 0b46921..8151caa 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -65,31 +65,10 @@
public final class ShapesKt {
}
- public final class TabColors {
- }
-
- public final class TabDefaults {
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- field public static final androidx.tv.material3.TabDefaults INSTANCE;
- }
-
public final class TabKt {
- method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- }
-
- public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public void TabSeparator();
- method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public long contentColor();
- method public long getContainerColor();
- property public final long ContainerColor;
- field public static final androidx.tv.material3.TabRowDefaults INSTANCE;
}
public final class TabRowKt {
- method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
}
public final class TextKt {
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 475b193..69a09ad 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -188,20 +188,20 @@
public final class ShapesKt {
}
- public final class TabColors {
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabColors {
}
- public final class TabDefaults {
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabDefaults {
+ method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long contentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledActiveContentColor, optional long disabledContentColor, optional long disabledSelectedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long contentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledActiveContentColor, optional long disabledContentColor, optional long disabledSelectedContentColor);
field public static final androidx.tv.material3.TabDefaults INSTANCE;
}
public final class TabKt {
- method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
- public final class TabRowDefaults {
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
method @androidx.compose.runtime.Composable public void TabSeparator();
method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
@@ -212,7 +212,7 @@
}
public final class TabRowKt {
- method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
}
public final class TextKt {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 0b46921..8151caa 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -65,31 +65,10 @@
public final class ShapesKt {
}
- public final class TabColors {
- }
-
- public final class TabDefaults {
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- field public static final androidx.tv.material3.TabDefaults INSTANCE;
- }
-
public final class TabKt {
- method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- }
-
- public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public void TabSeparator();
- method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public long contentColor();
- method public long getContainerColor();
- property public final long ContainerColor;
- field public static final androidx.tv.material3.TabRowDefaults INSTANCE;
}
public final class TabRowKt {
- method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
}
public final class TextKt {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
index 0a835b2..fd811f2 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
@@ -149,6 +149,7 @@
rule.onNodeWithTag(firstTab).assertIsFocused()
}
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Test
fun tabRow_changeActiveTabOnClick() {
val tabs = constructTabs(count = 2)
@@ -227,6 +228,7 @@
}
}
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun TabRowSample(
tabs: List<String>,
@@ -288,6 +290,7 @@
}
}
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun TabSample(
selected: Boolean,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
index 792f0c9..bed889f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
@@ -27,7 +27,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
@@ -60,6 +62,7 @@
* and customize the appearance / behavior of this tab in different states.
* @param content content of the [Tab]
*/
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@Composable
fun Tab(
selected: Boolean,
@@ -71,11 +74,13 @@
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) {
+ var isFocused by remember { mutableStateOf(false) }
val contentColor by
animateColorAsState(
getTabContentColor(
colors = colors,
- anyTabFocused = LocalTabRowHasFocus.current,
+ isTabRowActive = LocalTabRowHasFocus.current,
+ focused = isFocused,
selected = selected,
enabled = enabled,
)
@@ -89,6 +94,7 @@
this.role = Role.Tab
}
.onFocusChanged {
+ isFocused = it.isFocused
if (it.isFocused) {
onFocus()
}
@@ -114,12 +120,16 @@
* - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
* using an Underlined indicator
*/
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
class TabColors
internal constructor(
private val activeContentColor: Color,
+ private val contentColor: Color = activeContentColor.copy(alpha = 0.4f),
private val selectedContentColor: Color,
private val focusedContentColor: Color,
+ private val focusedSelectedContentColor: Color,
private val disabledActiveContentColor: Color,
+ private val disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
private val disabledSelectedContentColor: Color,
) {
/**
@@ -130,8 +140,7 @@
* @param enabled whether the button is enabled
*/
internal fun inactiveContentColor(enabled: Boolean): Color {
- return if (enabled) activeContentColor.copy(alpha = 0.4f)
- else disabledActiveContentColor.copy(alpha = 0.4f)
+ return if (enabled) contentColor else disabledContentColor
}
/**
@@ -160,87 +169,133 @@
* Represents the content color for this tab, depending on whether it is focused
*
* * [Tab] is focused when the current [Tab] is selected and focused
+ *
+ * @param selected whether the tab is selected
*/
- internal fun focusedContentColor(): Color {
- return focusedContentColor
+ internal fun focusedContentColor(selected: Boolean): Color {
+ return if (selected) focusedSelectedContentColor else focusedContentColor
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is TabColors) return false
- if (activeContentColor != other.activeContentColor(true)) return false
- if (selectedContentColor != other.selectedContentColor(true)) return false
- if (focusedContentColor != other.focusedContentColor()) return false
-
- if (disabledActiveContentColor != other.activeContentColor(false)) return false
- if (disabledSelectedContentColor != other.selectedContentColor(false)) return false
+ if (activeContentColor != other.activeContentColor) return false
+ if (contentColor != other.contentColor) return false
+ if (selectedContentColor != other.selectedContentColor) return false
+ if (focusedContentColor != other.focusedContentColor) return false
+ if (focusedSelectedContentColor != other.focusedSelectedContentColor) return false
+ if (disabledActiveContentColor != other.disabledActiveContentColor) return false
+ if (disabledContentColor != other.disabledContentColor) return false
+ if (disabledSelectedContentColor != other.disabledSelectedContentColor) return false
return true
}
override fun hashCode(): Int {
var result = activeContentColor.hashCode()
+ result = 31 * result + contentColor.hashCode()
result = 31 * result + selectedContentColor.hashCode()
result = 31 * result + focusedContentColor.hashCode()
+ result = 31 * result + focusedSelectedContentColor.hashCode()
result = 31 * result + disabledActiveContentColor.hashCode()
+ result = 31 * result + disabledContentColor.hashCode()
result = 31 * result + disabledSelectedContentColor.hashCode()
return result
}
}
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
object TabDefaults {
/**
* [Tab]'s content colors to in conjunction with underlined indicator
+ *
+ * @param activeContentColor applied when the any of the other tabs is focused
+ * @param contentColor the default color of the tab's content when none of the tabs are focused
+ * @param selectedContentColor applied when the current tab is selected
+ * @param focusedContentColor applied when the current tab is focused
+ * @param focusedSelectedContentColor applied when the current tab is both focused and selected
+ * @param disabledActiveContentColor applied when any of the other tabs is focused and the
+ * current tab is disabled
+ * @param disabledContentColor applied when the current tab is disabled and none of the tabs are
+ * focused
+ * @param disabledSelectedContentColor applied when the current tab is disabled and selected
*/
- // TODO: get selected & focused values from theme
+ // TODO: get selectedContentColor & focusedContentColor values from theme
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun underlinedIndicatorTabColors(
activeContentColor: Color = LocalContentColor.current,
+ contentColor: Color = activeContentColor.copy(alpha = 0.4f),
selectedContentColor: Color = Color(0xFFC9C2E8),
focusedContentColor: Color = Color(0xFFC9BFFF),
+ focusedSelectedContentColor: Color = focusedContentColor,
disabledActiveContentColor: Color = activeContentColor,
+ disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
disabledSelectedContentColor: Color = selectedContentColor,
): TabColors =
TabColors(
activeContentColor = activeContentColor,
+ contentColor = contentColor,
selectedContentColor = selectedContentColor,
focusedContentColor = focusedContentColor,
+ focusedSelectedContentColor = focusedSelectedContentColor,
disabledActiveContentColor = disabledActiveContentColor,
+ disabledContentColor = disabledContentColor,
disabledSelectedContentColor = disabledSelectedContentColor,
)
/**
* [Tab]'s content colors to in conjunction with pill indicator
+ *
+ * @param activeContentColor applied when the any of the other tabs is focused
+ * @param contentColor the default color of the tab's content when none of the tabs are focused
+ * @param selectedContentColor applied when the current tab is selected
+ * @param focusedContentColor applied when the current tab is focused
+ * @param focusedSelectedContentColor applied when the current tab is both focused and selected
+ * @param disabledActiveContentColor applied when any of the other tabs is focused and the
+ * current tab is disabled
+ * @param disabledContentColor applied when the current tab is disabled and none of the tabs are
+ * focused
+ * @param disabledSelectedContentColor applied when the current tab is disabled and selected
*/
// TODO: get selected & focused values from theme
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun pillIndicatorTabColors(
activeContentColor: Color = LocalContentColor.current,
+ contentColor: Color = activeContentColor.copy(alpha = 0.4f),
selectedContentColor: Color = Color(0xFFE5DEFF),
focusedContentColor: Color = Color(0xFF313033),
+ focusedSelectedContentColor: Color = focusedContentColor,
disabledActiveContentColor: Color = activeContentColor,
+ disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
disabledSelectedContentColor: Color = selectedContentColor,
): TabColors =
TabColors(
activeContentColor = activeContentColor,
+ contentColor = contentColor,
selectedContentColor = selectedContentColor,
focusedContentColor = focusedContentColor,
+ focusedSelectedContentColor = focusedSelectedContentColor,
disabledActiveContentColor = disabledActiveContentColor,
+ disabledContentColor = disabledContentColor,
disabledSelectedContentColor = disabledSelectedContentColor,
)
}
/** Returns the [Tab]'s content color based on focused/selected state */
+@OptIn(ExperimentalTvMaterial3Api::class)
private fun getTabContentColor(
colors: TabColors,
- anyTabFocused: Boolean,
+ isTabRowActive: Boolean,
+ focused: Boolean,
selected: Boolean,
enabled: Boolean,
): Color =
when {
- anyTabFocused && selected -> colors.focusedContentColor()
+ focused -> colors.focusedContentColor(selected)
selected -> colors.selectedContentColor(enabled)
- anyTabFocused -> colors.activeContentColor(enabled)
+ isTabRowActive -> colors.activeContentColor(enabled)
else -> colors.inactiveContentColor(enabled)
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
index d07a6a7..a1af7e7 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
@@ -78,6 +78,7 @@
* @param indicator used to indicate which tab is currently selected and/or focused
* @param tabs a composable which will render all the tabs
*/
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@Composable
fun TabRow(
selectedTabIndex: Int,
@@ -161,6 +162,7 @@
}
}
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
object TabRowDefaults {
/** Color of the background of a tab */
val ContainerColor = Color.Transparent
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/action.proto b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
index 9406086..72e7e95 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/action.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
@@ -4,6 +4,7 @@
package androidx.wear.protolayout.proto;
import "state.proto";
+import "state_entry.proto";
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "ActionProto";
@@ -72,10 +73,10 @@
AndroidActivity android_activity = 1;
}
-// An action used to load (or reload) the tile contents.
+// An action used to load (or reload) the layout contents.
message LoadAction {
- // The state to load the next tile with. This will be included in the
- // TileRequest sent after this action is invoked by a Clickable.
+ // The state to load the next layout with. This will be included in the
+ // layout request sent after this action is invoked by a Clickable.
State request_state = 1;
}
@@ -86,3 +87,19 @@
LoadAction load_action = 2;
}
}
+
+// An action that is handled internal to the current layout and won't cause a
+// layout refresh.
+message LocalAction {
+ oneof value {
+ SetStateAction set_state = 1;
+ }
+}
+
+// An action that sets a new value for a State
+message SetStateAction {
+ // The target key of the state item for this action.
+ string target_key = 1;
+ // The value to set the state item to, when this action is executed.
+ androidx.wear.protolayout.expression.proto.StateEntryValue value = 2;
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/alignment.proto b/wear/protolayout/protolayout-proto/src/main/proto/alignment.proto
new file mode 100644
index 0000000..aaec628
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/src/main/proto/alignment.proto
@@ -0,0 +1,150 @@
+// Alignment messages and enumerations, for use in other elements.
+syntax = "proto3";
+
+package androidx.wear.protolayout.proto;
+
+option java_package = "androidx.wear.protolayout.proto";
+option java_outer_classname = "AlignmentProto";
+
+// The horizontal alignment of an element within its container.
+enum HorizontalAlignment {
+ // Horizontal alignment is undefined.
+ HORIZONTAL_ALIGN_UNDEFINED = 0;
+
+ // Horizontally align to the left.
+ HORIZONTAL_ALIGN_LEFT = 1;
+
+ // Horizontally align to center.
+ HORIZONTAL_ALIGN_CENTER = 2;
+
+ // Horizontally align to the right.
+ HORIZONTAL_ALIGN_RIGHT = 3;
+
+ // Horizontally align to the content start (left in LTR layouts, right in RTL
+ // layouts).
+ HORIZONTAL_ALIGN_START = 4;
+
+ // Horizontally align to the content end (right in LTR layouts, left in RTL
+ // layouts).
+ HORIZONTAL_ALIGN_END = 5;
+}
+
+// An extensible HorizontalAlignment property.
+message HorizontalAlignmentProp {
+ // The value
+ HorizontalAlignment value = 1;
+}
+
+// The vertical alignment of an element within its container.
+enum VerticalAlignment {
+ // Vertical alignment is undefined.
+ VERTICAL_ALIGN_UNDEFINED = 0;
+
+ // Vertically align to the top.
+ VERTICAL_ALIGN_TOP = 1;
+
+ // Vertically align to center.
+ VERTICAL_ALIGN_CENTER = 2;
+
+ // Vertically align to the bottom.
+ VERTICAL_ALIGN_BOTTOM = 3;
+}
+
+// An extensible VerticalAlignment property.
+message VerticalAlignmentProp {
+ // The value.
+ VerticalAlignment value = 1;
+}
+
+// Alignment of a text element.
+enum TextAlignment {
+ // Alignment is undefined.
+ TEXT_ALIGN_UNDEFINED = 0;
+
+ // Align to the "start" of the Text element (left in LTR layouts, right in
+ // RTL layouts).
+ TEXT_ALIGN_START = 1;
+
+ // Align to the center of the Text element.
+ TEXT_ALIGN_CENTER = 2;
+
+ // Align to the "end" of the Text element (right in LTR layouts, left in RTL
+ // layouts).
+ TEXT_ALIGN_END = 3;
+}
+
+// An extensible TextAlignment property.
+message TextAlignmentProp {
+ // The value.
+ TextAlignment value = 1;
+}
+
+// The anchor position of an Arc's elements. This is used to specify how
+// elements added to an Arc should be laid out with respect to anchor_angle.
+//
+// As an example, assume that the following diagrams are wrapped to an arc, and
+// each represents an Arc element containing a single Text element. The Text
+// element's anchor_angle is "0" for all cases.
+//
+// ```
+// ARC_ANCHOR_START:
+// -180 0 180
+// Hello World!
+//
+//
+// ARC_ANCHOR_CENTER:
+// -180 0 180
+// Hello World!
+//
+// ARC_ANCHOR_END:
+// -180 0 180
+// Hello World!
+// ```
+enum ArcAnchorType {
+ // Anchor position is undefined.
+ ARC_ANCHOR_UNDEFINED = 0;
+
+ // Anchor at the start of the elements. This will cause elements added to an
+ // arc to begin at the given anchor_angle, and sweep around to the right.
+ ARC_ANCHOR_START = 1;
+
+ // Anchor at the center of the elements. This will cause the center of the
+ // whole set of elements added to an arc to be pinned at the given
+ // anchor_angle.
+ ARC_ANCHOR_CENTER = 2;
+
+ // Anchor at the end of the elements. This will cause the set of elements
+ // inside the arc to end at the specified anchor_angle, i.e. all elements
+ // should be to the left of anchor_angle.
+ ARC_ANCHOR_END = 3;
+}
+
+// An extensible ArcAnchorType property.
+message ArcAnchorTypeProp {
+ // The value.
+ ArcAnchorType value = 1;
+}
+
+// How to lay out components in a Arc context when they are smaller than their
+// container. This would be similar to HorizontalAlignment in a Box or Column.
+enum AngularAlignment {
+ // Angular alignment is undefined.
+ ANGULAR_ALIGNMENT_UNDEFINED = 0;
+
+ // Align to the start of the container. As an example, if the container
+ // starts at 90 degrees and has 180 degrees of sweep, the element within
+ // would draw from 90 degrees, clockwise.
+ ANGULAR_ALIGNMENT_START = 1;
+
+ // Align to the center of the container. As an example, if the container
+ // starts at 90 degrees, and has 180 degrees of sweep, and the contained
+ // element has 90 degrees of sweep, the element would draw between 135 and
+ // 225 degrees.
+ ANGULAR_ALIGNMENT_CENTER = 2;
+
+ // Align to the end of the container. As an example, if the container
+ // starts at 90 degrees and has 180 degrees of sweep, and the contained
+ // element has 90 degrees of sweep, the element would draw between 180 and 270
+ // degrees.
+ ANGULAR_ALIGNMENT_END = 3;
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/color.proto b/wear/protolayout/protolayout-proto/src/main/proto/color.proto
index f719a06..f5535d9 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/color.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/color.proto
@@ -3,11 +3,16 @@
package androidx.wear.protolayout.proto;
+import "dynamic.proto";
+
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "ColorProto";
// A property defining a color.
message ColorProp {
// The color value, in ARGB format.
- uint32 argb = 1;
+ optional uint32 argb = 1;
+
+ // The dynamic value.
+ androidx.wear.protolayout.expression.proto.DynamicColor dynamic_value = 2;
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto b/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
index e7a5e2a..25c9e37 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
@@ -1,12 +1,14 @@
-// Request messages used to fetch tiles and resources
+// Request messages used to fetch layouts and resources
syntax = "proto3";
package androidx.wear.protolayout.proto;
+import "version.proto";
+
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "DeviceParametersProto";
-// The platform of the device requesting a tile.
+// The platform of the device requesting a layout.
enum DevicePlatform {
// Device platform is undefined.
DEVICE_PLATFORM_UNDEFINED = 0;
@@ -27,7 +29,7 @@
SCREEN_SHAPE_RECT = 2;
}
-// Parameters describing the device requesting a tile update. This contains
+// Parameters describing the device requesting a layout update. This contains
// physical and logical characteristics about the device (e.g. screen size and
// density, etc).
message DeviceParameters {
@@ -41,9 +43,28 @@
// Pixels (px = dp * density).
float screen_density = 3;
+ // Current user preference for the scaling factor for fonts displayed on the
+ // display. This value is used to get from SP to DP (dp = sp * font_scale).
+ float font_scale = 7;
+
// The platform of the device.
DevicePlatform device_platform = 4;
// The shape of the device's screen
ScreenShape screen_shape = 5;
+
+ // The maximum schema version supported by the current renderer.
+ VersionInfo renderer_schema_version = 6;
+
+ // Renderer supported capabilities
+ Capabilities capabilities = 8;
+}
+
+// Capabilities describing the features that the renderer supports.
+message Capabilities {
+ // Current minimum freshness limit in milliseconds. This can change based on
+ // various factors. Any freshness request lower than the current limit will be
+ // replaced by that limit. A value of 0 here signifies that the minimum
+ // freshness limit in unknown.
+ uint64 minimum_freshness_limit_millis = 1;
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto b/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto
index a0a9989..6c69366 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/dimension.proto
@@ -3,13 +3,34 @@
package androidx.wear.protolayout.proto;
+import "alignment.proto";
+import "dynamic.proto";
+import "types.proto";
+
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "DimensionProto";
// A type for linear dimensions, measured in dp.
message DpProp {
// The value, in dp.
- float value = 1;
+ optional float value = 1;
+
+ // The dynamic value, in dp.
+ androidx.wear.protolayout.expression.proto.DynamicFloat dynamic_value = 2;
+
+ float value_for_layout = 3;
+
+ oneof align {
+ // Vertical alignment of the actual content within the space reserved by
+ // value_for_layout. Only valid when this DpProp is used for vertical
+ // sizing. If not specified, defaults to center alignment.
+ VerticalAlignment vertical_alignment_for_layout = 4;
+
+ // Horizontal alignment of the actual content within the space reserved by
+ // value_for_layout. Only valid when this DpProp is used for horizontal
+ // sizing. If not specified, defaults to center alignment.
+ HorizontalAlignment horizontal_alignment_for_layout = 5;
+ }
}
// A type for font sizes, measured in sp.
@@ -26,19 +47,62 @@
float value = 1;
}
+// The length of an ArcLine.
+message ArcLineLength {
+ oneof inner {
+ DegreesProp degrees = 1;
+ ExpandedAngularDimensionProp expanded_angular_dimension = 2;
+ }
+}
+
+// The length of an ArcSpacer.
+message ArcSpacerLength {
+ oneof inner {
+ DegreesProp degrees = 1;
+ ExpandedAngularDimensionProp expanded_angular_dimension = 2;
+ }
+}
+
+// A type for an angular dimension that fills all the space it can
+// (i.e. MATCH_PARENT in Android parlance)
+message ExpandedAngularDimensionProp {
+ // The layout weight (a dimensionless scalar value) for this element. The
+ // angular dimension for this element will be layout_weight times the
+ // available space divided by the sum of the layout_weights. If not set this
+ // defaults to 1.
+ FloatProp layout_weight = 1;
+}
+
// A type for angular dimensions, measured in degrees.
message DegreesProp {
// The value, in degrees.
- float value = 1;
+ optional float value = 1;
+
+ // The dynamic value, in degrees.
+ androidx.wear.protolayout.expression.proto.DynamicFloat dynamic_value = 2;
+
+ float value_for_layout = 3;
+ AngularAlignment angular_alignment_for_layout = 4;
}
// A type for a dimension that fills all the space it can (i.e. MATCH_PARENT in
// Android parlance)
-message ExpandedDimensionProp {}
+message ExpandedDimensionProp {
+ // The layout weight (a dimensionless scalar value) for this element. This
+ // will only affect the width of children of a Row or the height of children
+ // of a Column. By default, all children have equal weight. Where applicable,
+ // the width or height of the element is proportional to the sum of the
+ // weights of its siblings.
+ FloatProp layout_weight = 1;
+}
// A type for a dimension that sizes itself to the size of its children (i.e.
// WRAP_CONTENT in Android parlance)
-message WrappedDimensionProp {}
+message WrappedDimensionProp {
+ // The minimum size of this dimension. If not set, then there is no minimum
+ // size.
+ DpProp minimum_size = 1;
+}
// A type for a dimension that scales itself proportionally to another dimension
// such that the aspect ratio defined by the given width and height values is
@@ -79,6 +143,5 @@
message SpacerDimension {
oneof inner {
DpProp linear_dimension = 1;
- // TODO(b/169137847): Add ExpandedDimensionProp
}
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto b/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto
index 7a2a878..f09c002 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/fixed.proto
@@ -1,4 +1,5 @@
-// Fixed primitive types used by layout elements.
+// Fixed value primitive types that can be used in dynamic expressions and in
+// for state state values.
syntax = "proto3";
package androidx.wear.protolayout.expression.proto;
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index bd896ba..5307e19 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -4,64 +4,16 @@
package androidx.wear.protolayout.proto;
+import "alignment.proto";
import "color.proto";
import "dimension.proto";
+import "fingerprint.proto";
import "modifiers.proto";
import "types.proto";
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "LayoutElementProto";
-// The horizontal alignment of an element within its container.
-enum HorizontalAlignment {
- // Horizontal alignment is undefined.
- HORIZONTAL_ALIGN_UNDEFINED = 0;
-
- // Horizontally align to the left.
- HORIZONTAL_ALIGN_LEFT = 1;
-
- // Horizontally align to center.
- HORIZONTAL_ALIGN_CENTER = 2;
-
- // Horizontally align to the right.
- HORIZONTAL_ALIGN_RIGHT = 3;
-
- // Horizontally align to the content start (left in LTR layouts, right in RTL
- // layouts).
- HORIZONTAL_ALIGN_START = 4;
-
- // Horizontally align to the content end (right in LTR layouts, left in RTL
- // layouts).
- HORIZONTAL_ALIGN_END = 5;
-}
-
-// An extensible HorizontalAlignment property.
-message HorizontalAlignmentProp {
- // The value
- HorizontalAlignment value = 1;
-}
-
-// The vertical alignment of an element within its container.
-enum VerticalAlignment {
- // Vertical alignment is undefined.
- VERTICAL_ALIGN_UNDEFINED = 0;
-
- // Vertically align to the top.
- VERTICAL_ALIGN_TOP = 1;
-
- // Vertically align to center.
- VERTICAL_ALIGN_CENTER = 2;
-
- // Vertically align to the bottom.
- VERTICAL_ALIGN_BOTTOM = 3;
-}
-
-// An extensible VerticalAlignment property.
-message VerticalAlignmentProp {
- // The value.
- VerticalAlignment value = 1;
-}
-
// The weight to be applied to the font.
enum FontWeight {
// Font weight is undefined.
@@ -159,29 +111,6 @@
FontVariantProp variant = 7;
}
-// Alignment of a text element.
-enum TextAlignment {
- // Alignment is undefined.
- TEXT_ALIGN_UNDEFINED = 0;
-
- // Align to the "start" of the Text element (left in LTR layouts, right in
- // RTL layouts).
- TEXT_ALIGN_START = 1;
-
- // Align to the center of the Text element.
- TEXT_ALIGN_CENTER = 2;
-
- // Align to the "end" of the Text element (right in LTR layouts, left in RTL
- // layouts).
- TEXT_ALIGN_END = 3;
-}
-
-// An extensible TextAlignment property.
-message TextAlignmentProp {
- // The value.
- TextAlignment value = 1;
-}
-
// How text that will not fit inside the bounds of a Text element will be
// handled.
enum TextOverflow {
@@ -195,6 +124,13 @@
// Truncate the text to fit in the Text element's bounds, but add an ellipsis
// (i.e. ...) to the end of the text if it has been truncated.
TEXT_OVERFLOW_ELLIPSIZE_END = 2;
+
+ // Enable marquee animation for texts that don't fit inside the Text element.
+ // This is only applicable for single line texts; if the text has multiple
+ // lines, the behavior is equivalent to TEXT_OVERFLOW_TRUNCATE.
+ //
+
+ TEXT_OVERFLOW_MARQUEE = 3;
}
// An extensible TextOverflow property.
@@ -203,50 +139,12 @@
TextOverflow value = 1;
}
-// The anchor position of an Arc's elements. This is used to specify how
-// elements added to an Arc should be laid out with respect to anchor_angle.
-//
-// As an example, assume that the following diagrams are wrapped to an arc, and
-// each represents an Arc element containing a single Text element. The Text
-// element's anchor_angle is "0" for all cases.
-//
-// ```
-// ARC_ANCHOR_START:
-// -180 0 180
-// Hello World!
-//
-//
-// ARC_ANCHOR_CENTER:
-// -180 0 180
-// Hello World!
-//
-// ARC_ANCHOR_END:
-// -180 0 180
-// Hello World!
-// ```
-enum ArcAnchorType {
- // Anchor position is undefined.
- ARC_ANCHOR_UNDEFINED = 0;
-
- // Anchor at the start of the elements. This will cause elements added to an
- // arc to begin at the given anchor_angle, and sweep around to the right.
- ARC_ANCHOR_START = 1;
-
- // Anchor at the center of the elements. This will cause the center of the
- // whole set of elements added to an arc to be pinned at the given
- // anchor_angle.
- ARC_ANCHOR_CENTER = 2;
-
- // Anchor at the end of the elements. This will cause the set of elements
- // inside the arc to end at the specified anchor_angle, i.e. all elements
- // should be to the left of anchor_angle.
- ARC_ANCHOR_END = 3;
-}
-
-// An extensible ArcAnchorType property.
-message ArcAnchorTypeProp {
- // The value.
- ArcAnchorType value = 1;
+// An Android platform specific text style configuration options for styling and
+// compatibility.
+message AndroidTextStyle {
+ // Whether the Text excludes extra top and bottom padding above the normal
+ // ascent and descent. The default is false.
+ bool exclude_font_padding = 1;
}
// A text string.
@@ -283,6 +181,10 @@
// vertical distance between subsequent baselines. If not specified, defaults
// the font's recommended interline spacing.
SpProp line_height = 7;
+
+ // An Android platform specific text style configuration options for styling
+ // and compatibility.
+ AndroidTextStyle android_text_style = 8;
}
// How content which does not match the dimensions of its bounds (e.g. an image
@@ -356,7 +258,7 @@
// Filtering parameters for this image. If not specified, defaults to no
// filtering.
- ColorFilter filter = 6;
+ ColorFilter color_filter = 6;
}
// A simple spacer, typically used to provide padding between adjacent elements.
@@ -413,6 +315,10 @@
// Modifiers for this element.
SpanModifiers modifiers = 3;
+
+ // An Android platform specific text style configuration options for styling
+ // and compatibility.
+ AndroidTextStyle android_text_style = 4;
}
// An image which can be added to a Span.
@@ -445,11 +351,11 @@
}
}
-// A container of Span elements. Currently, this only supports Text elements,
-// where each individual Span can have different styling applied to it but the
-// resulting text will flow naturally. This allows sections of a paragraph of
-// text to have different styling applied to it, for example, making one or two
-// words bold or italic.
+// A container of Span elements. Currently, this supports SpanImage and SpanText
+// elements, where each individual Span can have different styling applied to it
+// but the resulting text will flow naturally. This allows sections of a
+// paragraph of text to have different styling applied to it, for example,
+// making one or two words bold or italic.
message Spannable {
// The Span elements that form this Spannable.
repeated Span spans = 1;
@@ -579,6 +485,12 @@
// Modifiers for this element.
Modifiers modifiers = 5;
+
+ // The target angle that will be used by the layout when expanding child
+ // elements. NB a value of zero is interpreted to mean 360 degrees. This
+ // target may not be achievable if other non-expandable elements bring us past
+ // this value.
+ DegreesProp max_angle = 6;
}
// A text element that can be used in an Arc.
@@ -607,6 +519,31 @@
// Modifiers for this element.
ArcModifiers modifiers = 4;
+
+ // The length of this line. If not defined, defaults to 0 degrees.
+ ArcLineLength angular_length = 5;
+
+ // The line stroke cap. If not defined, defaults to STROKE_CAP_ROUND.
+ StrokeCapProp stroke_cap = 6;
+}
+
+// Styles to use for path endings
+enum StrokeCap {
+ // StrokeCap is undefined.
+ STROKE_CAP_UNDEFINED = 0;
+
+ // Begin and end contours with a flat edge and no extension.
+ STROKE_CAP_BUTT = 1;
+
+ // Begin and end contours with a semi-circle extension. The extension size is
+ // proportional to the thickness on the path.
+ STROKE_CAP_ROUND = 2;
+}
+
+// An extensible StrokeCap property.
+message StrokeCapProp {
+ // The value.
+ StrokeCap value = 1;
}
// A simple spacer used to provide padding between adjacent elements in an Arc.
@@ -619,6 +556,9 @@
// Modifiers for this element.
ArcModifiers modifiers = 3;
+
+ // The length of this spacer. If not defined, defaults to 0 degrees.
+ ArcSpacerLength angular_length = 4;
}
// A container that allows a standard LayoutElement to be added to an Arc.
@@ -664,6 +604,12 @@
// A complete layout.
message Layout {
+ // The layout message itself doesn't need a fingerprint. The "fingerprint"
+ // field below represents the tree defined by the "root" element.
+
// The root element in the layout.
LayoutElement root = 1;
+
+ // The fingerprint for the tree starting at "root".
+ TreeFingerprint fingerprint = 1000;
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto b/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
index 4aa04db..d6f9586 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
@@ -4,6 +4,7 @@
package androidx.wear.protolayout.proto;
import "action.proto";
+import "animation_parameters.proto";
import "color.proto";
import "dimension.proto";
import "types.proto";
@@ -23,13 +24,52 @@
Action on_click = 2;
}
+// The type of user interface element. Accessibility services might use this to
+// describe the element or do customizations.
+enum SemanticsRole {
+ // Role is undefined. It may be automatically populated.
+ SEMANTICS_ROLE_NONE = 0;
+
+ // The element is an Image.
+ SEMANTICS_ROLE_IMAGE = 1;
+
+ // The element is a Button control.
+ SEMANTICS_ROLE_BUTTON = 2;
+
+ // The element is a Checkbox which is a component that represents two states
+ // (checked / unchecked).
+ SEMANTICS_ROLE_CHECKBOX = 3;
+
+ // The element is a Switch which is a two state toggleable component that
+ // provides on/off like options.
+ SEMANTICS_ROLE_SWITCH = 4;
+
+ // This element is a RadioButton which is a component to represent two states,
+ // selected and not selected.
+ SEMANTICS_ROLE_RADIOBUTTON = 5;
+}
+
// A modifier for an element which has accessibility semantics associated with
// it. This should generally be used sparingly, and in most cases should only be
// applied to the top-level layout element or to Clickables.
message Semantics {
// The content description associated with this element. This will be dictated
// when the element is focused by the screen reader.
- string content_description = 1;
+ // @deprecated Use content_description instead.
+ string obsolete_content_description = 1;
+
+ // The content description associated with this element. This will be dictated
+ // when the element is focused by the screen reader.
+ StringProp content_description = 4;
+
+ // The type of user interface element. Accessibility services might use this
+ // to describe the element or do customizations.
+ SemanticsRole role = 2;
+
+ // The localized state description of the semantics node.
+ // For example: "on" or "off". This will be dictated when the element is
+ // focused by the screen reader.
+ StringProp state_description = 3;
}
// A modifier to apply padding around an element.
@@ -95,26 +135,192 @@
// Padding or Background), or change their behaviour (e.g. Clickable, or
// Semantics).
message Modifiers {
- // Allows its wrapped element to have actions associated with it, which will
- // be executed when the element is tapped.
+ // The clickable property of the modified element. It allows its wrapped
+ // element to have actions associated with it, which will be executed when the
+ // element is tapped.
Clickable clickable = 1;
- // Adds metadata for the modified element, for example, screen reader content
- // descriptions.
+ // The semantics of the modified element. This can be used to add metadata to
+ // the modified element (eg. screen reader content descriptions).
Semantics semantics = 2;
- // Adds padding to the modified element.
+ // The padding of the modified element.
Padding padding = 3;
- // Draws a border around the modified element.
+ // The border of the modified element.
Border border = 4;
- // Adds a background (with corner radius) to the modified element.
+ // The background (with optional corner radius) of the modified element.
Background background = 5;
// Metadata about an element. For use by libraries building higher-level
// components only. This can be used to track component metadata
ElementMetadata metadata = 6;
+
+ // The content transition of an element. Any update to the element or its
+ // children will trigger this animation for this element and everything
+ // underneath it.
+ AnimatedVisibility content_update_animation = 7;
+
+ // Whether the attached element is hidden, or visible. If the element is
+ // hidden, then it will still consume space in the layout, but will not render
+ // any contents, nor will any children render any contents.
+ //
+ // Note that a hidden element also cannot be clickable (i.e. a Clickable
+ // modifier would be ignored).
+ //
+ // Defaults to false (i.e. not hidden).
+ BoolProp hidden = 8;
+}
+
+// The content transition of an element. Any update to the element or its
+// children will trigger this animation for this element and everything
+// underneath it.
+message AnimatedVisibility {
+ // The content transition that is triggered when element enters the layout.
+ EnterTransition enter = 1;
+
+ // The content transition that is triggered when element exits the layout.
+ // Note that indefinite exit animations are ignored.
+ ExitTransition exit = 2;
+}
+
+// The content transition that is triggered when element enters the layout.
+message EnterTransition {
+ // The fading in animation for content transition of an element and its
+ // children happening when entering the layout.
+ FadeInTransition fade_in = 1;
+
+ // The sliding in animation for content transition of an element and its
+ // children happening when entering the layout.
+ SlideInTransition slide_in = 2;
+}
+
+// The fading animation for content transition of an element and its children,
+// from the specified starting alpha to fully visible.
+message FadeInTransition {
+ // The starting alpha of the fade in transition. It should be between 0 and 1.
+ // If not set, defaults to fully transparent, i.e. 0.
+ float initial_alpha = 1;
+
+ // The animation parameters for duration, delay, etc.
+ androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 2;
+}
+
+// The sliding in animation for content transition of an element and its
+// children.
+message SlideInTransition {
+ // The slide direction used for slide animations on any element, from the
+ // specified point to its destination in the layout. If not set, defaults to
+ // horizontal from left to the right.
+ SlideDirection direction = 1;
+
+ // The initial offset for animation. By default the transition starts from the
+ // left parent boundary for horizontal orientation and from the top for
+ // vertical orientation. Note that sliding from the screen boundaries can only
+ // be achieved if all parent's sizes are big enough to accommodate it.
+ SlideBound initial_slide_bound = 2;
+
+ // The animation parameters for duration, delay, etc.
+ androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 3;
+}
+
+// The content transition that is triggered when element exits the layout.
+message ExitTransition {
+ // The fading out animation for content transition of an element and its
+ // children happening when exiting the layout.
+ FadeOutTransition fade_out = 1;
+
+ // The sliding out animation for content transition of an element and its
+ // children happening when exiting the layout.
+ SlideOutTransition slide_out = 2;
+}
+
+// The fading animation for content transition of an element and its children,
+// from fully visible to the specified target alpha.
+message FadeOutTransition {
+ // The target alpha of the fade out transition. It should be between 0 and 1.
+ // If not set, defaults to fully invisible, i.e. 0.
+ float target_alpha = 1;
+
+ // The animation parameters for duration, delay, etc.
+ androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 2;
+}
+
+// The sliding out animation for content transition of an element and its
+// children.
+message SlideOutTransition {
+ // The slide direction used for slide animations on any element, from its
+ // destination in the layout to the specified point. If not set, defaults to
+ // horizontal from right to the left.
+ SlideDirection direction = 1;
+
+ // The target offset for animation. By default the transition will end at the
+ // left parent boundary for horizontal orientation and at the top for
+ // vertical orientation. Note that sliding from the screen boundaries can only
+ // be achieved if all parent's sizes are big enough to accommodate it.
+ SlideBound target_slide_bound = 2;
+
+ // The animation parameters for duration, delay, etc.
+ androidx.wear.protolayout.expression.proto.AnimationSpec animation_spec = 3;
+}
+
+// The boundary that a Slide animation will use for start/end.
+message SlideBound {
+ oneof inner {
+ SlideParentBound parent_bound = 1;
+ SlideLinearBound linear_bound = 2;
+ }
+}
+
+// The slide animation will animate from/to the parent elements boundaries.
+message SlideParentBound {
+ // The snap options to use when sliding using parent boundaries. Defaults to
+ // SLIDE_PARENT_SNAP_TO_INSIDE if not specified.
+ SlideParentSnapOption snap_to = 1;
+}
+
+// The snap options to use when sliding using parent boundaries.
+enum SlideParentSnapOption {
+ // The undefined snapping option.
+ SLIDE_PARENT_SNAP_UNDEFINED = 0;
+
+ // The option that snaps insides of the element and its parent at start/end.
+ SLIDE_PARENT_SNAP_TO_INSIDE = 1;
+
+ // The option that snaps outsides of the element and its parent at start/end.
+ SLIDE_PARENT_SNAP_TO_OUTSIDE = 2;
+}
+
+// The slide animation will use the explicit offset for the animation boundary.
+message SlideLinearBound {
+ // The absolute delta linear distance to animate from/to. A direction will be
+ // defined by SlideDirection and this value should be a positive offset.
+ float offset_dp = 1;
+}
+
+// The slide direction used for slide animations on any element, from the
+// specified point to its destination in the layout for in animation or reverse
+// for out animation.
+enum SlideDirection {
+ // The undefined sliding orientation.
+ SLIDE_DIRECTION_UNDEFINED = 0;
+
+ // The sliding orientation that moves an element horizontally from left to the
+ // right.
+ SLIDE_DIRECTION_LEFT_TO_RIGHT = 1;
+
+ // The sliding orientation that moves an element horizontally from right to
+ // the left.
+ SLIDE_DIRECTION_RIGHT_TO_LEFT = 2;
+
+ // The sliding orientation that moves an element vertically from top to the
+ // bottom.
+ SLIDE_DIRECTION_TOP_TO_BOTTOM = 3;
+
+ // The sliding orientation that moves an element vertically from bottom to the
+ // top.
+ SLIDE_DIRECTION_BOTTOM_TO_TOP = 4;
}
// Modifiers that can be used with ArcLayoutElements. These may change the way
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
index f14a6a21..3284bee 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
@@ -3,6 +3,9 @@
package androidx.wear.protolayout.proto;
+import "dynamic.proto";
+import "trigger.proto";
+
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "ResourceProto";
@@ -44,6 +47,56 @@
ImageFormat format = 4;
}
+// An image to be loaded from a ContentProvider, by content URI.
+message AndroidImageResourceByContentUri {
+ // The content URI to use, on the format content://authority/resource.
+ string content_uri = 1;
+}
+
+// Format describing the contents of an animated image.
+enum AnimatedImageFormat {
+ // An undefined image format.
+ ANIMATED_IMAGE_FORMAT_UNDEFINED = 0;
+
+ // Android AnimatedVectorDrawable.
+ ANIMATED_IMAGE_FORMAT_AVD = 1;
+}
+
+// A non-seekable animated image resource that maps to an Android drawable by
+// resource ID. The animation is started with given trigger, fire and forget.
+message AndroidAnimatedImageResourceByResId {
+ // The format for the animated image.
+ AnimatedImageFormat format = 1;
+
+ // The Android resource ID, e.g. R.drawable.foo.
+ int32 resource_id = 2;
+
+ // The trigger to start the animation.
+ Trigger trigger = 3;
+}
+
+// A seekable animated image resource that maps to an Android drawable by
+// resource ID. The animation progress is bound to the provided dynamic float.
+message AndroidSeekableAnimatedImageResourceByResId {
+ // The format for the animated image.
+ AnimatedImageFormat format = 1;
+
+ // The Android resource ID, e.g. R.drawable.foo
+ int32 resource_id = 2;
+
+ // A dynamic float, normally transformed from certain states with the data
+ // binding pipeline, controls the progress of the animation. Its value is
+ // required to fall in the range of [0.0, 1.0], any values outside this range
+ // would be clamped.
+ //
+ // Typically, AnimatableFixedFloat or AnimatableDynamicFloat is used for this
+ // progress. With AnimatableFixedFloat, the animation is played from progress
+ // of its from_value to to_value; with AnimatableDynamicFloat, the animation
+ // is set from progress 0 to its first value once it is available, it then
+ // plays from current progress to the new value on subsequent updates.
+ androidx.wear.protolayout.expression.proto.DynamicFloat progress = 3;
+}
+
// An image resource, which can be used by layouts. This holds multiple
// underlying resource types, which the underlying runtime will pick according
// to what it thinks is appropriate.
@@ -53,18 +106,30 @@
// An image resource that contains the image data inline.
InlineImageResource inline_resource = 2;
+
+ // An image which loads its drawable via an Android Content URI.
+ AndroidImageResourceByContentUri android_content_uri = 5;
+
+ // A non-seekable animated image resource that maps to an Android drawable by
+ // resource ID. The animation is started with given trigger, fire and forget.
+ AndroidAnimatedImageResourceByResId android_animated_resource_by_res_id = 6;
+
+ // A seekable animated image resource that maps to an Android drawable by
+ // resource ID. The animation progress is bound to the provided dynamic float.
+ AndroidSeekableAnimatedImageResourceByResId
+ android_seekable_animated_resource_by_res_id = 7;
}
// The resources for a layout.
message Resources {
// The version of this Resources instance.
//
- // Each tile specifies the version of resources it requires. After fetching a
- // tile, the renderer will use the resources version specified by the tile
- // to separately fetch the resources.
+ // Each layout specifies the version of resources it requires. After fetching
+ // a layout, the renderer will use the resources version specified by the
+ // layout to separately fetch the resources.
//
- // This value must match the version of the resources required by the tile
- // for the tile to render successfully, and must match the resource version
+ // This value must match the version of the resources required by the layout
+ // for the layout to render successfully, and must match the resource version
// specified in ResourcesRequest which triggered this request.
string version = 1;
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/state.proto b/wear/protolayout/protolayout-proto/src/main/proto/state.proto
index 64d63468..162077e 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/state.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/state.proto
@@ -1,8 +1,10 @@
-// State of a tile.
+// State of a layout.
syntax = "proto3";
package androidx.wear.protolayout.proto;
+import "state_entry.proto";
+
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "StateProto";
@@ -10,4 +12,8 @@
message State {
// The ID of the clickable that was last clicked.
string last_clickable_id = 1;
+
+ // Any shared state between the provider and renderer.
+ map<string, androidx.wear.protolayout.expression.proto.StateEntryValue>
+ id_to_value = 2;
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/trigger.proto b/wear/protolayout/protolayout-proto/src/main/proto/trigger.proto
new file mode 100644
index 0000000..ef42469
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/src/main/proto/trigger.proto
@@ -0,0 +1,37 @@
+// Triggers that can be used to start an animation.
+syntax = "proto3";
+
+package androidx.wear.protolayout.proto;
+
+import "dynamic.proto";
+
+option java_package = "androidx.wear.protolayout.proto";
+option java_outer_classname = "TriggerProto";
+
+// Triggers when the layout visibility state turns from invisible to fully
+// visible.
+message OnVisibleTrigger {}
+
+// Triggers only once when the layout visibility state turns from invisible to
+// fully visible for the first time.
+message OnVisibleOnceTrigger {}
+
+// Triggers immediately when the layout is loaded / reloaded.
+message OnLoadTrigger {}
+
+// Triggers *every time* the condition switches from false to true. If the
+// condition is true initially, that will fire the trigger on load.
+message OnConditionTrigger {
+ // Dynamic boolean used as trigger.
+ androidx.wear.protolayout.expression.proto.DynamicBool dynamic_bool = 1;
+}
+
+// The triggers that can be fired.
+message Trigger {
+ oneof inner {
+ OnVisibleTrigger on_visible_trigger = 1;
+ OnVisibleOnceTrigger on_visible_once_trigger = 2;
+ OnLoadTrigger on_load_trigger = 3;
+ OnConditionTrigger on_condition_trigger = 4;
+ }
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/types.proto b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
index 00591bc..c9ddacc 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/types.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
@@ -3,6 +3,9 @@
package androidx.wear.protolayout.proto;
+import "alignment.proto";
+import "dynamic.proto";
+
option java_package = "androidx.wear.protolayout.proto";
option java_outer_classname = "TypesProto";
@@ -10,22 +13,42 @@
message Int32Prop {
// The value.
int32 value = 1;
+
+ // The dynamic value.
+ androidx.wear.protolayout.expression.proto.DynamicInt32 dynamic_value = 2;
}
// A string type.
message StringProp {
// The value.
string value = 1;
+
+ // The dynamic value.
+ androidx.wear.protolayout.expression.proto.DynamicString dynamic_value = 2;
+
+ // When used as a layout-changing data bind, the string to measure, when
+ // considering how wide the element should be in the layout.
+ string value_for_layout = 3;
+
+ // Alignment alignment of the actual text within the space reserved by
+ // value_for_layout. If not specified, defaults to center alignment.
+ TextAlignment text_alignment_for_layout = 4;
}
// A float type.
message FloatProp {
// The value.
- float value = 1;
+ optional float value = 1;
+
+ // The dynamic value.
+ androidx.wear.protolayout.expression.proto.DynamicFloat dynamic_value = 2;
}
// A boolean type.
message BoolProp {
// The value.
bool value = 1;
+
+ // The dynamic value.
+ androidx.wear.protolayout.expression.proto.DynamicBool dynamic_value = 2;
}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/version.proto b/wear/protolayout/protolayout-proto/src/main/proto/version.proto
index 4a006bb..7c581e3 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/version.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/version.proto
@@ -1,4 +1,4 @@
-// The components of a tile that can be rendered by a tile renderer.
+// The schema version information of a layout.
syntax = "proto3";
package androidx.wear.protolayout.proto;
@@ -7,7 +7,7 @@
option java_outer_classname = "VersionProto";
// Version information. This is used to encode the schema version of a payload
-// (e.g. inside of Tile).
+// (e.g. inside of a layout).
message VersionInfo {
// Major version. Incremented on breaking changes (i.e. compatibility is not
// guaranteed across major versions).
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
index 07e6ea9..f0938c10 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
@@ -68,6 +68,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.core.content.ContextCompat;
+import androidx.wear.protolayout.proto.AlignmentProto;
import androidx.wear.tiles.TileService;
import androidx.wear.protolayout.proto.ActionProto.Action;
import androidx.wear.protolayout.proto.ActionProto.AndroidActivity;
@@ -84,7 +85,7 @@
import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
import androidx.wear.protolayout.proto.DimensionProto.WrappedDimensionProp;
import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
-import androidx.wear.protolayout.proto.LayoutElementProto.ArcAnchorTypeProp;
+import androidx.wear.protolayout.proto.AlignmentProto.ArcAnchorTypeProp;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLine;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcSpacer;
@@ -94,7 +95,7 @@
import androidx.wear.protolayout.proto.LayoutElementProto.ContentScaleMode;
import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle;
import androidx.wear.protolayout.proto.LayoutElementProto.FontVariant;
-import androidx.wear.protolayout.proto.LayoutElementProto.HorizontalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
import androidx.wear.protolayout.proto.LayoutElementProto.Image;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
@@ -106,9 +107,9 @@
import androidx.wear.protolayout.proto.LayoutElementProto.SpanVerticalAlignmentProp;
import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
import androidx.wear.protolayout.proto.LayoutElementProto.Text;
-import androidx.wear.protolayout.proto.LayoutElementProto.TextAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.TextAlignmentProp;
import androidx.wear.protolayout.proto.LayoutElementProto.TextOverflowProp;
-import androidx.wear.protolayout.proto.LayoutElementProto.VerticalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
import androidx.wear.protolayout.proto.ModifiersProto.ArcModifiers;
import androidx.wear.protolayout.proto.ModifiersProto.Background;
import androidx.wear.protolayout.proto.ModifiersProto.Border;
@@ -680,7 +681,7 @@
}
if (modifiers.hasSemantics()) {
- applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+ applyAudibleParams(view, modifiers.getSemantics().getObsoleteContentDescription());
}
if (modifiers.hasPadding()) {
@@ -721,7 +722,7 @@
}
if (modifiers.hasSemantics()) {
- applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+ applyAudibleParams(view, modifiers.getSemantics().getObsoleteContentDescription());
}
return view;
@@ -751,6 +752,7 @@
return null;
case TEXT_OVERFLOW_ELLIPSIZE_END:
return TruncateAt.END;
+ case TEXT_OVERFLOW_MARQUEE:
case TEXT_OVERFLOW_UNDEFINED:
case UNRECOGNIZED:
return TEXT_OVERFLOW_DEFAULT;
@@ -1253,10 +1255,11 @@
Log.wtf(TAG, "Exception tinting image " + protoResId, ex);
}
- if (image.hasFilter()) {
- if (image.getFilter().hasTint() && canImageBeTinted) {
+ if (image.hasColorFilter()) {
+ if (image.getColorFilter().hasTint() && canImageBeTinted) {
// Only allow tinting for Android images.
- ColorStateList tint = ColorStateList.valueOf(image.getFilter().getTint().getArgb());
+ ColorStateList tint =
+ ColorStateList.valueOf(image.getColorFilter().getTint().getArgb());
imageView.setImageTintList(tint);
// SRC_IN throws away the colours in the drawable that we're tinting. Effectively,
diff --git a/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java b/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
index 01c37cf..8f65bd4f 100644
--- a/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
+++ b/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
@@ -67,8 +67,8 @@
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
import androidx.wear.protolayout.proto.LayoutElementProto.Column;
import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle;
-import androidx.wear.protolayout.proto.LayoutElementProto.HorizontalAlignment;
-import androidx.wear.protolayout.proto.LayoutElementProto.HorizontalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignment;
+import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
import androidx.wear.protolayout.proto.LayoutElementProto.Image;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
@@ -79,8 +79,8 @@
import androidx.wear.protolayout.proto.LayoutElementProto.SpanText;
import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
import androidx.wear.protolayout.proto.LayoutElementProto.Text;
-import androidx.wear.protolayout.proto.LayoutElementProto.VerticalAlignment;
-import androidx.wear.protolayout.proto.LayoutElementProto.VerticalAlignmentProp;
+import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment;
+import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
import androidx.wear.protolayout.proto.ModifiersProto.Border;
import androidx.wear.protolayout.proto.ModifiersProto.Clickable;
import androidx.wear.protolayout.proto.ModifiersProto.Modifiers;
@@ -176,8 +176,9 @@
.setText(StringProp.newBuilder().setValue(textContents))
.setModifiers(Modifiers.newBuilder()
.setSemantics(Semantics.newBuilder()
- .setContentDescription("Hello World Text Element"))))
- .build();
+ .setObsoleteContentDescription(
+ "Hello World Text Element"))))
+ .build();
FrameLayout rootLayout = inflateProto(root);
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
index 3a811ba..58b2dee 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/LayoutElementBuilders.java
@@ -27,6 +27,7 @@
import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.proto.AlignmentProto;
import androidx.wear.tiles.ColorBuilders.ColorProp;
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.tiles.DimensionBuilders.ContainerDimension;
@@ -341,9 +342,9 @@
/** An extensible {@code HorizontalAlignment} property. */
public static final class HorizontalAlignmentProp {
- private final LayoutElementProto.HorizontalAlignmentProp mImpl;
+ private final AlignmentProto.HorizontalAlignmentProp mImpl;
- private HorizontalAlignmentProp(LayoutElementProto.HorizontalAlignmentProp impl) {
+ private HorizontalAlignmentProp(AlignmentProto.HorizontalAlignmentProp impl) {
this.mImpl = impl;
}
@@ -357,28 +358,28 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static HorizontalAlignmentProp fromProto(
- @NonNull LayoutElementProto.HorizontalAlignmentProp proto) {
+ @NonNull AlignmentProto.HorizontalAlignmentProp proto) {
return new HorizontalAlignmentProp(proto);
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- public LayoutElementProto.HorizontalAlignmentProp toProto() {
+ public AlignmentProto.HorizontalAlignmentProp toProto() {
return mImpl;
}
/** Builder for {@link HorizontalAlignmentProp} */
public static final class Builder {
- private final LayoutElementProto.HorizontalAlignmentProp.Builder mImpl =
- LayoutElementProto.HorizontalAlignmentProp.newBuilder();
+ private final AlignmentProto.HorizontalAlignmentProp.Builder mImpl =
+ AlignmentProto.HorizontalAlignmentProp.newBuilder();
public Builder() {}
/** Sets the value. */
@NonNull
public Builder setValue(@HorizontalAlignment int value) {
- mImpl.setValue(LayoutElementProto.HorizontalAlignment.forNumber(value));
+ mImpl.setValue(AlignmentProto.HorizontalAlignment.forNumber(value));
return this;
}
@@ -392,9 +393,9 @@
/** An extensible {@code VerticalAlignment} property. */
public static final class VerticalAlignmentProp {
- private final LayoutElementProto.VerticalAlignmentProp mImpl;
+ private final AlignmentProto.VerticalAlignmentProp mImpl;
- private VerticalAlignmentProp(LayoutElementProto.VerticalAlignmentProp impl) {
+ private VerticalAlignmentProp(AlignmentProto.VerticalAlignmentProp impl) {
this.mImpl = impl;
}
@@ -408,28 +409,28 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static VerticalAlignmentProp fromProto(
- @NonNull LayoutElementProto.VerticalAlignmentProp proto) {
+ @NonNull AlignmentProto.VerticalAlignmentProp proto) {
return new VerticalAlignmentProp(proto);
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- public LayoutElementProto.VerticalAlignmentProp toProto() {
+ public AlignmentProto.VerticalAlignmentProp toProto() {
return mImpl;
}
/** Builder for {@link VerticalAlignmentProp} */
public static final class Builder {
- private final LayoutElementProto.VerticalAlignmentProp.Builder mImpl =
- LayoutElementProto.VerticalAlignmentProp.newBuilder();
+ private final AlignmentProto.VerticalAlignmentProp.Builder mImpl =
+ AlignmentProto.VerticalAlignmentProp.newBuilder();
public Builder() {}
/** Sets the value. */
@NonNull
public Builder setValue(@VerticalAlignment int value) {
- mImpl.setValue(LayoutElementProto.VerticalAlignment.forNumber(value));
+ mImpl.setValue(AlignmentProto.VerticalAlignment.forNumber(value));
return this;
}
@@ -840,9 +841,9 @@
/** An extensible {@code TextAlignment} property. */
public static final class TextAlignmentProp {
- private final LayoutElementProto.TextAlignmentProp mImpl;
+ private final AlignmentProto.TextAlignmentProp mImpl;
- private TextAlignmentProp(LayoutElementProto.TextAlignmentProp impl) {
+ private TextAlignmentProp(AlignmentProto.TextAlignmentProp impl) {
this.mImpl = impl;
}
@@ -856,28 +857,28 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static TextAlignmentProp fromProto(
- @NonNull LayoutElementProto.TextAlignmentProp proto) {
+ @NonNull AlignmentProto.TextAlignmentProp proto) {
return new TextAlignmentProp(proto);
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- public LayoutElementProto.TextAlignmentProp toProto() {
+ public AlignmentProto.TextAlignmentProp toProto() {
return mImpl;
}
/** Builder for {@link TextAlignmentProp} */
public static final class Builder {
- private final LayoutElementProto.TextAlignmentProp.Builder mImpl =
- LayoutElementProto.TextAlignmentProp.newBuilder();
+ private final AlignmentProto.TextAlignmentProp.Builder mImpl =
+ AlignmentProto.TextAlignmentProp.newBuilder();
public Builder() {}
/** Sets the value. */
@NonNull
public Builder setValue(@TextAlignment int value) {
- mImpl.setValue(LayoutElementProto.TextAlignment.forNumber(value));
+ mImpl.setValue(AlignmentProto.TextAlignment.forNumber(value));
return this;
}
@@ -942,9 +943,9 @@
/** An extensible {@code ArcAnchorType} property. */
public static final class ArcAnchorTypeProp {
- private final LayoutElementProto.ArcAnchorTypeProp mImpl;
+ private final AlignmentProto.ArcAnchorTypeProp mImpl;
- private ArcAnchorTypeProp(LayoutElementProto.ArcAnchorTypeProp impl) {
+ private ArcAnchorTypeProp(AlignmentProto.ArcAnchorTypeProp impl) {
this.mImpl = impl;
}
@@ -958,28 +959,28 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static ArcAnchorTypeProp fromProto(
- @NonNull LayoutElementProto.ArcAnchorTypeProp proto) {
+ @NonNull AlignmentProto.ArcAnchorTypeProp proto) {
return new ArcAnchorTypeProp(proto);
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- public LayoutElementProto.ArcAnchorTypeProp toProto() {
+ public AlignmentProto.ArcAnchorTypeProp toProto() {
return mImpl;
}
/** Builder for {@link ArcAnchorTypeProp} */
public static final class Builder {
- private final LayoutElementProto.ArcAnchorTypeProp.Builder mImpl =
- LayoutElementProto.ArcAnchorTypeProp.newBuilder();
+ private final AlignmentProto.ArcAnchorTypeProp.Builder mImpl =
+ AlignmentProto.ArcAnchorTypeProp.newBuilder();
public Builder() {}
/** Sets the value. */
@NonNull
public Builder setValue(@ArcAnchorType int value) {
- mImpl.setValue(LayoutElementProto.ArcAnchorType.forNumber(value));
+ mImpl.setValue(AlignmentProto.ArcAnchorType.forNumber(value));
return this;
}
@@ -1195,9 +1196,9 @@
@NonNull
public Builder setMultilineAlignment(@TextAlignment int multilineAlignment) {
mImpl.setMultilineAlignment(
- LayoutElementProto.TextAlignmentProp.newBuilder()
+ AlignmentProto.TextAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.TextAlignment.forNumber(
+ AlignmentProto.TextAlignment.forNumber(
multilineAlignment)));
return this;
}
@@ -1452,8 +1453,8 @@
*/
@Nullable
public ColorFilter getColorFilter() {
- if (mImpl.hasFilter()) {
- return ColorFilter.fromProto(mImpl.getFilter());
+ if (mImpl.hasColorFilter()) {
+ return ColorFilter.fromProto(mImpl.getColorFilter());
} else {
return null;
}
@@ -1556,7 +1557,7 @@
*/
@NonNull
public Builder setColorFilter(@NonNull ColorFilter filter) {
- mImpl.setFilter(filter.toProto());
+ mImpl.setColorFilter(filter.toProto());
return this;
}
@@ -1836,9 +1837,9 @@
@NonNull
public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
mImpl.setHorizontalAlignment(
- LayoutElementProto.HorizontalAlignmentProp.newBuilder()
+ AlignmentProto.HorizontalAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.HorizontalAlignment.forNumber(
+ AlignmentProto.HorizontalAlignment.forNumber(
horizontalAlignment)));
return this;
}
@@ -1859,9 +1860,9 @@
@NonNull
public Builder setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
mImpl.setVerticalAlignment(
- LayoutElementProto.VerticalAlignmentProp.newBuilder()
+ AlignmentProto.VerticalAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.VerticalAlignment.forNumber(
+ AlignmentProto.VerticalAlignment.forNumber(
verticalAlignment)));
return this;
}
@@ -2396,9 +2397,9 @@
@NonNull
public Builder setMultilineAlignment(@HorizontalAlignment int multilineAlignment) {
mImpl.setMultilineAlignment(
- LayoutElementProto.HorizontalAlignmentProp.newBuilder()
+ AlignmentProto.HorizontalAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.HorizontalAlignment.forNumber(
+ AlignmentProto.HorizontalAlignment.forNumber(
multilineAlignment)));
return this;
}
@@ -2586,9 +2587,9 @@
@NonNull
public Builder setHorizontalAlignment(@HorizontalAlignment int horizontalAlignment) {
mImpl.setHorizontalAlignment(
- LayoutElementProto.HorizontalAlignmentProp.newBuilder()
+ AlignmentProto.HorizontalAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.HorizontalAlignment.forNumber(
+ AlignmentProto.HorizontalAlignment.forNumber(
horizontalAlignment)));
return this;
}
@@ -2762,9 +2763,9 @@
@NonNull
public Builder setVerticalAlignment(@VerticalAlignment int verticalAlignment) {
mImpl.setVerticalAlignment(
- LayoutElementProto.VerticalAlignmentProp.newBuilder()
+ AlignmentProto.VerticalAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.VerticalAlignment.forNumber(
+ AlignmentProto.VerticalAlignment.forNumber(
verticalAlignment)));
return this;
}
@@ -2951,8 +2952,8 @@
@NonNull
public Builder setAnchorType(@ArcAnchorType int anchorType) {
mImpl.setAnchorType(
- LayoutElementProto.ArcAnchorTypeProp.newBuilder()
- .setValue(LayoutElementProto.ArcAnchorType.forNumber(anchorType)));
+ AlignmentProto.ArcAnchorTypeProp.newBuilder()
+ .setValue(AlignmentProto.ArcAnchorType.forNumber(anchorType)));
return this;
}
@@ -2976,9 +2977,9 @@
@NonNull
public Builder setVerticalAlign(@VerticalAlignment int verticalAlign) {
mImpl.setVerticalAlign(
- LayoutElementProto.VerticalAlignmentProp.newBuilder()
+ AlignmentProto.VerticalAlignmentProp.newBuilder()
.setValue(
- LayoutElementProto.VerticalAlignment.forNumber(
+ AlignmentProto.VerticalAlignment.forNumber(
verticalAlign)));
return this;
}
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java
index b3ced75..29fdc7e 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ModifiersBuilders.java
@@ -127,7 +127,7 @@
*/
@NonNull
public String getContentDescription() {
- return mImpl.getContentDescription();
+ return mImpl.getObsoleteContentDescription();
}
/** @hide */
@@ -157,7 +157,7 @@
*/
@NonNull
public Builder setContentDescription(@NonNull String contentDescription) {
- mImpl.setContentDescription(contentDescription);
+ mImpl.setObsoleteContentDescription(contentDescription);
return this;
}
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index e5d3585..442d906 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -43,6 +43,8 @@
import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
@@ -491,14 +493,12 @@
complicationInstanceId: Int,
) {
mainThreadCoroutineScope.launch {
- data.collect { evaluatedData ->
- if (evaluatedData == null) return@collect // Ignore pre-evaluation.
- close() // Doing one-off evaluation, the service will be re-invoked.
- iComplicationManager.updateComplicationData(
- complicationInstanceId,
- evaluatedData
- )
- }
+ // Doing one-off evaluation, the service will be re-invoked.
+ iComplicationManager.updateComplicationData(
+ complicationInstanceId,
+ data.filterNotNull().first()
+ )
+ close()
}
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 7dbbbac..a8ef73db 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -177,6 +177,13 @@
other is ComplicationData &&
asWireComplicationData() == other.asWireComplicationData()
+ /**
+ * Similar to [equals], but avoids comparing evaluated fields (if expressions exist).
+ * @hide
+ */
+ infix fun equalsUnevaluated(other: ComplicationData): Boolean =
+ asWireComplicationData() equalsUnevaluated other.asWireComplicationData()
+
override fun hashCode(): Int = asWireComplicationData().hashCode()
/**
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
index 38680ef..5cf9133 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
@@ -39,6 +39,7 @@
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -191,6 +192,7 @@
}
@Test
+ @Ignore // TODO(b/265425077): This test is failing on the bots, fix it.
fun roundTripUserStyleSchema() = runBlocking {
val service = IStyleEchoService.Stub.asInterface(
bindService(Intent(ACTON).apply { setPackage(PACKAGE_NAME) })
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 75a2093..565f2c9 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -28,32 +28,36 @@
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
+import androidx.wear.watchface.RenderParameters.HighlightedElement
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator
+import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
+import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.EmptyComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
-import androidx.wear.watchface.RenderParameters.HighlightedElement
-import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
-import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.toApiComplicationData
import androidx.wear.watchface.data.BoundingArcWireFormat
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
import java.lang.Integer.min
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlin.math.abs
-import kotlin.math.atan2
-import kotlin.math.cos
-import kotlin.math.sin
+import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
+import java.time.temporal.ChronoUnit.SECONDS
import java.util.Objects
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
/**
* Interface for rendering complicationSlots onto a [Canvas]. These should be created by
@@ -425,6 +429,8 @@
)
}
+ private val wearSdkVersion by lazy { complicationSlotsManager.watchFaceHostApi.wearSdkVersion }
+
private var lastComplicationUpdate = Instant.EPOCH
private class ComplicationDataHistoryEntry(
@@ -884,8 +890,10 @@
/**
* The default [ComplicationType] to use alongside [defaultDataSourcePolicy].
*/
- @Deprecated("Use DefaultComplicationDataSourcePolicy." +
- "systemDataSourceFallbackDefaultType instead")
+ @Deprecated(
+ "Use DefaultComplicationDataSourcePolicy." +
+ "systemDataSourceFallbackDefaultType instead"
+ )
public var defaultDataSourceType: ComplicationType = defaultDataSourceType
@UiThread
get
@@ -965,12 +973,16 @@
internal var dataDirty = true
+ private var lastExpressionEvaluator: ComplicationDataExpressionEvaluator? = null
+
+ private var unevaluatedComplicationData: ComplicationData = NoDataComplicationData()
+
/**
* The [androidx.wear.watchface.complications.data.ComplicationData] associated with the
* [ComplicationSlot]. This defaults to [NoDataComplicationData].
*/
public val complicationData: StateFlow<ComplicationData> =
- MutableStateFlow(NoDataComplicationData())
+ MutableStateFlow(unevaluatedComplicationData)
/**
* The complication data sent by the system. This may contain a timeline out of which
@@ -991,9 +1003,7 @@
lastComplicationUpdate = instant
complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant))
timelineComplicationData = complicationData
- timelineEntries = complicationData.asWireComplicationData().timelineEntries?.map {
- it
- }
+ timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
}
@@ -1034,12 +1044,80 @@
best = screenLockedFallback // This is NoDataComplicationData.
}
- if (forceUpdate || complicationData.value != best) {
- renderer.loadData(best, loadDrawablesAsynchronous)
- (complicationData as MutableStateFlow).value = best
+ if (wearSdkVersion >= Build.VERSION_CODES.TIRAMISU) {
+ best.evaluateAndLoadUpdates(loadDrawablesAsynchronous)
+ // Loading synchronously if forced.
+ if (forceUpdate) best.load(loadDrawablesAsynchronous)
+ } else {
+ // Avoid expression evaluation pre-T as it may be redacted by the old platform.
+ if (forceUpdate || complicationData.value != best) best.load(loadDrawablesAsynchronous)
}
}
+ private fun ComplicationData.load(loadDrawablesAsynchronous: Boolean) {
+ renderer.loadData(this, loadDrawablesAsynchronous)
+ (complicationData as MutableStateFlow).value = this
+ }
+
+ /**
+ * Creates a [ComplicationDataExpressionEvaluator] and monitors for updates, sending them to the
+ * [renderer].
+ *
+ * Ignores new data that has equivalent expression (see [ComplicationData.equalsUnevaluated]).
+ * While the data is first being evaluated, sends [NoDataComplicationData] to the renderer.
+ */
+ private fun ComplicationData.evaluateAndLoadUpdates(
+ loadDrawablesAsynchronous: Boolean,
+ ) {
+ if (unevaluatedComplicationData equalsUnevaluated this) return
+ unevaluatedComplicationData = this
+ // Reverting to NoData while evaluating.
+ NoDataComplicationData().load(loadDrawablesAsynchronous)
+ lastExpressionEvaluator?.close()
+ // TODO(b/260065006): Do we need to close the evaluator on destroy?
+ lastExpressionEvaluator =
+ ComplicationDataExpressionEvaluator(asWireComplicationData())
+ .apply {
+ init()
+ loadUpdates(loadDrawablesAsynchronous)
+ }
+ }
+
+ /**
+ * Monitors evaluated expression updates and sends them to the [renderer].
+ *
+ * If this is the first evaluation, loads the data immediately. Otherwise, triggers watchface
+ * invalidation on the next top of the second.
+ */
+ private fun ComplicationDataExpressionEvaluator.loadUpdates(
+ loadDrawablesAsynchronous: Boolean
+ ) {
+ complicationSlotsManager.watchFaceHostApi.getUiThreadCoroutineScope().launch {
+ data.collect { evaluatedWireData ->
+ if (evaluatedWireData == null) return@collect // Not yet evaluated.
+ val evaluatedData = evaluatedWireData.toApiComplicationData()
+ if (complicationData.value is NoDataComplicationData) {
+ // Loading now if it's the first update.
+ evaluatedData.load(loadDrawablesAsynchronous)
+ } else {
+ // Loading in the next frame on further updates.
+ (complicationData as MutableStateFlow).value = evaluatedData
+ complicationSlotsManager.watchFaceHostApi.postInvalidate(
+ durationUntilNextForcedFrame()
+ )
+ }
+ }
+ }
+ }
+
+ /** Returns the duration until the next top of the second. */
+ private fun durationUntilNextForcedFrame(): Duration {
+ val now = Instant.ofEpochMilli(
+ complicationSlotsManager.watchFaceHostApi.systemTimeProvider.getSystemTimeMillis()
+ )
+ return Duration.between(now, (now + Duration.ofSeconds(1)).truncatedTo(SECONDS))
+ }
+
/**
* Whether or not the complication should be considered active and should be rendered at the
* specified time.
@@ -1132,7 +1210,8 @@
if (isHeadless) {
timelineComplicationData = EmptyComplicationData()
- (complicationData as MutableStateFlow).value = EmptyComplicationData()
+ unevaluatedComplicationData = EmptyComplicationData()
+ (complicationData as MutableStateFlow).value = unevaluatedComplicationData
}
}
@@ -1202,20 +1281,26 @@
writer.println(
"defaultDataSourcePolicy.primaryDataSource=${defaultDataSourcePolicy.primaryDataSource}"
)
- writer.println("defaultDataSourcePolicy.primaryDataSourceDefaultDataSourceType=" +
- defaultDataSourcePolicy.primaryDataSourceDefaultType)
+ writer.println(
+ "defaultDataSourcePolicy.primaryDataSourceDefaultDataSourceType=" +
+ defaultDataSourcePolicy.primaryDataSourceDefaultType
+ )
writer.println(
"defaultDataSourcePolicy.secondaryDataSource=" +
defaultDataSourcePolicy.secondaryDataSource
)
- writer.println("defaultDataSourcePolicy.secondaryDataSourceDefaultDataSourceType=" +
- defaultDataSourcePolicy.secondaryDataSourceDefaultType)
+ writer.println(
+ "defaultDataSourcePolicy.secondaryDataSourceDefaultDataSourceType=" +
+ defaultDataSourcePolicy.secondaryDataSourceDefaultType
+ )
writer.println(
"defaultDataSourcePolicy.systemDataSourceFallback=" +
defaultDataSourcePolicy.systemDataSourceFallback
)
- writer.println("defaultDataSourcePolicy.systemDataSourceFallbackDefaultType=" +
- defaultDataSourcePolicy.systemDataSourceFallbackDefaultType)
+ writer.println(
+ "defaultDataSourcePolicy.systemDataSourceFallbackDefaultType=" +
+ defaultDataSourcePolicy.systemDataSourceFallbackDefaultType
+ )
writer.println("timelineComplicationData=$timelineComplicationData")
writer.println("timelineEntries=" + timelineEntries?.joinToString())
writer.println("data=${renderer.getData()}")
@@ -1245,8 +1330,10 @@
if (accessibilityTraversalIndex != other.accessibilityTraversalIndex) return false
if (boundsType != other.boundsType) return false
if (complicationSlotBounds != other.complicationSlotBounds) return false
- if (supportedTypes.size != other.supportedTypes.size ||
- !supportedTypes.containsAll(other.supportedTypes)) return false
+ if (
+ supportedTypes.size != other.supportedTypes.size ||
+ !supportedTypes.containsAll(other.supportedTypes)
+ ) return false
if (defaultDataSourcePolicy != other.defaultDataSourcePolicy) return false
if (initiallyEnabled != other.initiallyEnabled) return false
if (fixedComplicationDataSource != other.fixedComplicationDataSource) return false
@@ -1260,9 +1347,11 @@
override fun hashCode(): Int {
@OptIn(ComplicationExperimental::class)
- return Objects.hash(id, accessibilityTraversalIndex, boundsType, complicationSlotBounds,
+ return Objects.hash(
+ id, accessibilityTraversalIndex, boundsType, complicationSlotBounds,
supportedTypes.sorted(),
defaultDataSourcePolicy, initiallyEnabled, fixedComplicationDataSource,
- nameResourceId, screenReaderNameResourceId, boundingArc)
+ nameResourceId, screenReaderNameResourceId, boundingArc
+ )
}
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index 9553f47..a693dc2 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -199,11 +199,9 @@
/** Finish initialization. */
@WorkerThread
internal fun init(
- watchFaceHostApi: WatchFaceHostApi,
renderer: Renderer,
complicationSlotInvalidateListener: ComplicationSlot.InvalidateListener
) = TraceEvent("ComplicationSlotsManager.init").use {
- this.watchFaceHostApi = watchFaceHostApi
this.renderer = renderer
for ((_, complication) in complicationSlots) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
index 32d6ffa..4e70ee1 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceHostApi.kt
@@ -37,6 +37,17 @@
/** The [WatchFaceService.SystemTimeProvider]. */
public val systemTimeProvider: WatchFaceService.SystemTimeProvider
+ /**
+ * Equivalent to [android.os.Build.VERSION.SDK_INT], but allows override for any
+ * platform-independent versioning.
+ *
+ * This is meant to only be used in androidTest, which only support testing on one SDK. In
+ * Robolectric tests use `@Config(sdk = [Build.VERSION_CODES.*])`.
+ *
+ * Note that this cannot override platform-dependent versioning, which means inconsistency.
+ */
+ public val wearSdkVersion: Int
+
/** Returns the watch face's [Context]. */
public fun getContext(): Context
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 ca08131..8c1ff64 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
@@ -1237,6 +1237,7 @@
private lateinit var choreographer: ChoreographerWrapper
override val systemTimeProvider = getSystemTimeProvider()
+ override val wearSdkVersion = [email protected]
/**
* Whether we already have a [frameCallback] posted and waiting in the [Choreographer]
@@ -2091,6 +2092,7 @@
TraceEvent("WatchFaceService.createComplicationsManager").use {
createComplicationSlotsManager(currentUserStyleRepository)
}
+ complicationSlotsManager.watchFaceHostApi = this@EngineWrapper
complicationSlotsManager.watchState = watchState
complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
listenForComplicationChanges(complicationSlotsManager)
@@ -2315,7 +2317,7 @@
// to render soon anyway.
var initFinished = false
complicationSlotsManager.init(
- this, renderer,
+ renderer,
object : ComplicationSlot.InvalidateListener {
@SuppressWarnings("SyntheticAccessor")
override fun onInvalidate() {
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 aae4c30..fd9bfe2 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
@@ -16,6 +16,8 @@
package androidx.wear.watchface
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
@@ -35,8 +37,6 @@
import android.os.Looper
import android.os.Parcel
import android.provider.Settings
-import android.support.wearable.complications.ComplicationData
-import android.support.wearable.complications.ComplicationText
import android.support.wearable.watchface.Constants
import android.support.wearable.watchface.IWatchFaceService
import android.support.wearable.watchface.WatchFaceStyle
@@ -52,9 +52,11 @@
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
+import androidx.wear.watchface.complications.data.ComplicationText
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.CountUpTimeReference
import androidx.wear.watchface.complications.data.EmptyComplicationData
@@ -62,6 +64,7 @@
import androidx.wear.watchface.complications.data.NoDataComplicationData
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.complications.data.StringExpression
import androidx.wear.watchface.complications.data.TimeDifferenceComplicationText
import androidx.wear.watchface.complications.data.TimeDifferenceStyle
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -101,10 +104,15 @@
import kotlin.test.assertFailsWith
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -558,8 +566,8 @@
complication.id,
when (complication.defaultDataSourceType) {
ComplicationType.SHORT_TEXT ->
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Initial Short"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Initial Short"))
.setTapAction(
PendingIntent.getActivity(
context, 0, Intent("ShortText"),
@@ -569,8 +577,8 @@
.build()
ComplicationType.LONG_TEXT ->
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Initial Long"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Initial Long"))
.setTapAction(
PendingIntent.getActivity(
context, 0, Intent("LongText"),
@@ -580,7 +588,7 @@
.build()
ComplicationType.PHOTO_IMAGE ->
- ComplicationData.Builder(ComplicationData.TYPE_LARGE_IMAGE)
+ WireComplicationData.Builder(WireComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(Icon.createWithContentUri("someuri"))
.setTapAction(
PendingIntent.getActivity(
@@ -605,10 +613,23 @@
}
private fun initWallpaperInteractiveWatchFaceInstance(
- @WatchFaceType watchFaceType: Int,
- complicationSlots: List<ComplicationSlot>,
- userStyleSchema: UserStyleSchema,
- wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams,
+ @WatchFaceType watchFaceType: Int = WatchFaceType.ANALOG,
+ complicationSlots: List<ComplicationSlot> = emptyList(),
+ userStyleSchema: UserStyleSchema = UserStyleSchema(emptyList()),
+ wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams =
+ WallpaperInteractiveWatchFaceInstanceParams(
+ INTERACTIVE_INSTANCE_ID,
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ null
+ ),
complicationCache: MutableMap<String, ByteArray>? = null,
) {
testWatchFaceService = TestWatchFaceService(
@@ -661,8 +682,8 @@
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
- watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
- engineWrapper.deferredValidation.await()
+ watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
+ engineWrapper.deferredValidation.awaitWithTimeout()
}
currentUserStyleRepository = watchFaceImpl.currentUserStyleRepository
@@ -706,7 +727,7 @@
private fun setComplicationViaWallpaperCommand(
complicationSlotId: Int,
- complicationData: ComplicationData
+ complicationData: WireComplicationData
) {
engineWrapper.onCommand(
Constants.COMMAND_COMPLICATION_DATA,
@@ -1540,7 +1561,7 @@
TimeDifferenceStyle.STOPWATCH,
CountUpTimeReference(Instant.parse("2022-10-30T10:15:30.001Z"))
).setMinimumTimeUnit(TimeUnit.MINUTES).build(),
- androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ComplicationText.EMPTY
).build().asWireComplicationData()
)
)
@@ -1584,7 +1605,7 @@
TimeDifferenceStyle.STOPWATCH,
CountUpTimeReference(referenceInstant)
).setMinimumTimeUnit(TimeUnit.HOURS).build(),
- androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ComplicationText.EMPTY
).build().asWireComplicationData()
)
)
@@ -1605,7 +1626,7 @@
TimeDifferenceStyle.STOPWATCH,
CountUpTimeReference(referenceInstant)
).setMinimumTimeUnit(TimeUnit.SECONDS).build(),
- androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ComplicationText.EMPTY
).build().asWireComplicationData()
)
)
@@ -1967,8 +1988,8 @@
// We're only sending one complication, the others should default to empty.
setComplicationViaWallpaperCommand(
rightComplication.id,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Initial Short"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Initial Short"))
.build()
)
@@ -2251,7 +2272,7 @@
LEFT_COMPLICATION_ID,
listOf(dataSource1, dataSource2),
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
- ComplicationData.TYPE_SHORT_TEXT
+ WireComplicationData.TYPE_SHORT_TEXT
)
}
@@ -2285,15 +2306,15 @@
runPostedTasksFor(0)
verify(iWatchFaceService).setDefaultComplicationProvider(
- LEFT_COMPLICATION_ID, dataSource2, ComplicationData.TYPE_SHORT_TEXT
+ LEFT_COMPLICATION_ID, dataSource2, WireComplicationData.TYPE_SHORT_TEXT
)
verify(iWatchFaceService).setDefaultComplicationProvider(
- LEFT_COMPLICATION_ID, dataSource1, ComplicationData.TYPE_SHORT_TEXT
+ LEFT_COMPLICATION_ID, dataSource1, WireComplicationData.TYPE_SHORT_TEXT
)
verify(iWatchFaceService).setDefaultSystemComplicationProvider(
LEFT_COMPLICATION_ID,
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
- ComplicationData.TYPE_SHORT_TEXT
+ WireComplicationData.TYPE_SHORT_TEXT
)
}
@@ -2382,11 +2403,11 @@
)
assertThat(complicationDetails[0].complicationState.supportedTypes).isEqualTo(
intArrayOf(
- ComplicationData.TYPE_RANGED_VALUE,
- ComplicationData.TYPE_LONG_TEXT,
- ComplicationData.TYPE_SHORT_TEXT,
- ComplicationData.TYPE_ICON,
- ComplicationData.TYPE_SMALL_IMAGE
+ WireComplicationData.TYPE_RANGED_VALUE,
+ WireComplicationData.TYPE_LONG_TEXT,
+ WireComplicationData.TYPE_SHORT_TEXT,
+ WireComplicationData.TYPE_ICON,
+ WireComplicationData.TYPE_SMALL_IMAGE
)
)
@@ -2399,11 +2420,11 @@
)
assertThat(complicationDetails[1].complicationState.supportedTypes).isEqualTo(
intArrayOf(
- ComplicationData.TYPE_RANGED_VALUE,
- ComplicationData.TYPE_LONG_TEXT,
- ComplicationData.TYPE_SHORT_TEXT,
- ComplicationData.TYPE_ICON,
- ComplicationData.TYPE_SMALL_IMAGE
+ WireComplicationData.TYPE_RANGED_VALUE,
+ WireComplicationData.TYPE_LONG_TEXT,
+ WireComplicationData.TYPE_SHORT_TEXT,
+ WireComplicationData.TYPE_ICON,
+ WireComplicationData.TYPE_SMALL_IMAGE
)
)
@@ -2415,7 +2436,7 @@
Rect(0, 0, 100, 100)
)
assertThat(complicationDetails[2].complicationState.supportedTypes).isEqualTo(
- intArrayOf(ComplicationData.TYPE_LARGE_IMAGE)
+ intArrayOf(WireComplicationData.TYPE_LARGE_IMAGE)
)
}
@@ -2441,7 +2462,7 @@
listOf(leftComplication, rightComplication),
{ _, currentUserStyleRepository, watchState ->
// Prevent initialization until initDeferred completes.
- initDeferred.await()
+ initDeferred.awaitWithTimeout()
renderer = TestRenderer(
surfaceHolder,
currentUserStyleRepository,
@@ -2862,8 +2883,8 @@
)
)
- lateinit var leftComplicationData: ComplicationData
- lateinit var rightComplicationData: ComplicationData
+ lateinit var leftComplicationData: WireComplicationData
+ lateinit var rightComplicationData: WireComplicationData
val scope = CoroutineScope(Dispatchers.Main.immediate)
@@ -2883,26 +2904,75 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT")).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT")).build()
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
)
)
)
- assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
+ assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_LONG_TEXT)
assertThat(leftComplicationData.longText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_LONG_TEXT")
- assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+ assertThat(rightComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_SHORT_TEXT")
}
@Test
+ @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
+ public fun complicationDataExpression_evaluatesExpression(): Unit =
+ runBlocking(Dispatchers.Main.immediate) {
+ initWallpaperInteractiveWatchFaceInstance(
+ complicationSlots = listOf(leftComplication),
+ )
+
+ interactiveWatchFaceInstance.updateComplicationData(
+ listOf(
+ IdAndComplicationDataWireFormat(
+ LEFT_COMPLICATION_ID,
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
+ .build()
+ )
+ )
+ )
+
+ val data = leftComplication.complicationData.firstNonEmpty().asWireComplicationData()
+ assertThat(data.shortText)
+ // TODO(b/260065006): Verify that it is actually evaluated.
+ .isEqualTo(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.R])
+ public fun complicationDataExpression_preT_doesNotEvaluateExpression(): Unit =
+ runBlocking(Dispatchers.Main.immediate) {
+ initWallpaperInteractiveWatchFaceInstance(
+ complicationSlots = listOf(leftComplication),
+ )
+
+ interactiveWatchFaceInstance.updateComplicationData(
+ listOf(
+ IdAndComplicationDataWireFormat(
+ LEFT_COMPLICATION_ID,
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
+ .build()
+ )
+ )
+ )
+
+ val data = leftComplication.complicationData.firstNonEmpty().asWireComplicationData()
+ assertThat(data.shortText)
+ .isEqualTo(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
+ }
+
+ @Test
@Config(sdk = [Build.VERSION_CODES.O_MR1])
public fun complicationCache() {
val complicationCache = HashMap<String, ByteArray>()
@@ -2933,8 +3003,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
.setTapAction(
PendingIntent.getActivity(
context, 0, Intent("LongText"),
@@ -2945,8 +3015,8 @@
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
)
)
)
@@ -3008,7 +3078,7 @@
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
- val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.await()
+ val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.awaitWithTimeout()
// Check the ComplicationData was cached.
val leftComplicationData =
@@ -3020,11 +3090,11 @@
RIGHT_COMPLICATION_ID
]!!.complicationData.value.asWireComplicationData()
- assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
+ assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_LONG_TEXT)
assertThat(leftComplicationData.longText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_LONG_TEXT")
assertThat(leftComplicationData.tapActionLostDueToSerialization).isTrue()
- assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+ assertThat(rightComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_SHORT_TEXT")
assertThat(rightComplicationData.tapActionLostDueToSerialization).isFalse()
@@ -3058,8 +3128,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
.setTapAction(
PendingIntent.getActivity(
context, 0, Intent("LongText"),
@@ -3071,8 +3141,8 @@
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
)
)
)
@@ -3134,7 +3204,7 @@
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
- val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.await()
+ val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.awaitWithTimeout()
// Check only the right ComplicationData was cached.
val leftComplicationData =
@@ -3146,8 +3216,8 @@
RIGHT_COMPLICATION_ID
]!!.complicationData.value.asWireComplicationData()
- assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_NO_DATA)
- assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+ assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_NO_DATA)
+ assertThat(rightComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_SHORT_TEXT")
assertThat(rightComplicationData.tapActionLostDueToSerialization).isFalse()
@@ -3180,11 +3250,11 @@
assertThat(complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value.type)
.isEqualTo(ComplicationType.NO_DATA)
- val a = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("A"))
+ val a = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("A"))
.build()
- val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("B"))
+ val b = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("B"))
.build()
b.timelineStartEpochSecond = 1000
b.timelineEndEpochSecond = Long.MAX_VALUE
@@ -3251,7 +3321,7 @@
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
- val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.await()
+ val watchFaceImpl2 = engineWrapper2.deferredWatchFaceImpl.awaitWithTimeout()
watchFaceImpl2.complicationSlotsManager.selectComplicationDataForInstant(
Instant.ofEpochSecond(999)
@@ -3263,7 +3333,7 @@
LEFT_COMPLICATION_ID
]!!.complicationData.value.asWireComplicationData()
- assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+ assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
assertThat(leftComplicationData.shortText?.getTextAt(context.resources, 0))
.isEqualTo("A")
@@ -3276,7 +3346,7 @@
LEFT_COMPLICATION_ID
]!!.complicationData.value.asWireComplicationData()
- assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
+ assertThat(leftComplicationData.type).isEqualTo(WireComplicationData.TYPE_SHORT_TEXT)
assertThat(leftComplicationData.shortText?.getTextAt(context.resources, 0))
.isEqualTo("B")
}
@@ -3358,7 +3428,7 @@
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
- watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
+ watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
}
assertThat(
@@ -3395,8 +3465,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT"))
.build()
)
)
@@ -3410,7 +3480,7 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_EMPTY).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_EMPTY).build()
)
)
)
@@ -3422,8 +3492,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT"))
.setStartDateTimeMillis(1000000)
.setEndDateTimeMillis(2000000)
.build()
@@ -3464,8 +3534,8 @@
listOf(
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT"))
.build()
)
)
@@ -3760,8 +3830,8 @@
setComplicationViaWallpaperCommand(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Override"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Override"))
.build()
)
@@ -3797,8 +3867,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("INITIAL_VALUE"))
.build()
)
),
@@ -3809,8 +3879,8 @@
// This should be ignored because we're on the R flow.
setComplicationViaWallpaperCommand(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Override"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Override"))
.build()
)
@@ -3949,8 +4019,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("INITIAL_VALUE"))
.build()
)
),
@@ -4032,8 +4102,8 @@
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("INITIAL_VALUE"))
.build()
)
),
@@ -4136,7 +4206,7 @@
assertTrue(engineWrapper.deferredValidation.isCancelled)
runBlocking {
assertFailsWith<IllegalArgumentException> {
- engineWrapper.deferredValidation.await()
+ engineWrapper.deferredValidation.awaitWithTimeout()
}
}
}
@@ -4329,15 +4399,15 @@
mutableListOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("LEFT!"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("LEFT!"))
.setTapAction(leftPendingIntent)
.build()
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("RIGHT!"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("RIGHT!"))
.setTapAction(rightPendingIntent)
.build()
)
@@ -4925,7 +4995,7 @@
// Prevent initialization until initDeferred completes.
override suspend fun init() {
super.init()
- initDeferred.await()
+ initDeferred.awaitWithTimeout()
}
override fun onDestroy() {
@@ -5270,17 +5340,17 @@
@Test
@Config(sdk = [Build.VERSION_CODES.O_MR1])
public fun selectComplicationDataForInstant_overlapping() {
- val a = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("A"))
+ val a = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("A"))
.build()
- val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("B"))
+ val b = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("B"))
.build()
b.timelineStartEpochSecond = 1000
b.timelineEndEpochSecond = 4000
- val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("C"))
+ val c = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("C"))
.build()
c.timelineStartEpochSecond = 2000
c.timelineEndEpochSecond = 3000
@@ -5325,17 +5395,17 @@
@Test
@Config(sdk = [Build.VERSION_CODES.O_MR1])
public fun selectComplicationDataForInstant_disjoint() {
- val a = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("A"))
+ val a = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("A"))
.build()
- val b = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("B"))
+ val b = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("B"))
.build()
b.timelineStartEpochSecond = 1000
b.timelineEndEpochSecond = 2000
- val c = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("C"))
+ val c = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("C"))
.build()
c.timelineStartEpochSecond = 3000
c.timelineEndEpochSecond = 4000
@@ -5380,27 +5450,25 @@
@Test
@Config(sdk = [Build.VERSION_CODES.O_MR1])
public fun selectComplicationDataForInstant_timeLineWithPlaceholder() {
- val placeholderText =
- androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER
val timelineEntry =
- ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(placeholderText.toWireComplicationText())
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(ComplicationText.PLACEHOLDER.toWireComplicationText())
.build()
)
.build()
timelineEntry.timelineStartEpochSecond = 100
timelineEntry.timelineEndEpochSecond = 1000
- val wireLongTextComplication = ComplicationData.Builder(
+ val wireLongTextComplication = WireComplicationData.Builder(
ComplicationType.LONG_TEXT.toWireComplicationType()
)
.setEndDateTimeMillis(1650988800000)
.setDataSource(ComponentName("a", "b"))
- .setLongText(ComplicationText.plainText("longText"))
- .setSmallImageStyle(ComplicationData.IMAGE_STYLE_ICON)
- .setContentDescription(ComplicationText.plainText("test"))
+ .setLongText(WireComplicationText.plainText("longText"))
+ .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
+ .setContentDescription(WireComplicationText.plainText("test"))
.build()
wireLongTextComplication.setTimelineEntryCollection(listOf(timelineEntry))
@@ -5898,22 +5966,22 @@
val left1 = IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Left1"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Left1"))
.build()
)
val left2 = IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Left2"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Left2"))
.build()
)
val right = IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("Right"))
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("Right"))
.build()
)
@@ -5981,13 +6049,13 @@
val complicationList = listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
- .setLongText(ComplicationText.plainText("TYPE_LONG_TEXT")).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT")).build()
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
- ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
- .setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
)
)
@@ -6449,7 +6517,7 @@
// This shouldn't crash.
runBlocking {
- watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
+ watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
}
engineWrapper.onDestroy()
@@ -6487,7 +6555,7 @@
TimeDifferenceStyle.STOPWATCH,
CountUpTimeReference(Instant.parse("2022-10-30T10:15:30.001Z"))
).setMinimumTimeUnit(TimeUnit.MINUTES).build(),
- androidx.wear.watchface.complications.data.ComplicationText.EMPTY
+ ComplicationText.EMPTY
)
.setDisplayPolicy(
ComplicationDisplayPolicies.DO_NOT_SHOW_WHEN_DEVICE_LOCKED
@@ -6538,4 +6606,9 @@
WindowInsets.Type.systemBars(),
Insets.of(Rect().apply { bottom = chinHeight })
).build()
+
+ private suspend fun <T> Deferred<T>.awaitWithTimeout(): T = withTimeout(1000) { await() }
+
+ private suspend fun Flow<ComplicationData>.firstNonEmpty(): ComplicationData =
+ withTimeout(1000) { dropWhile { it is NoDataComplicationData }.first() }
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index 21a89ed..83b8023 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -619,10 +619,16 @@
database.insert("workspec", CONFLICT_FAIL, valuesTwo);
mMigrationTestHelper.runMigrationsAndValidate(TEST_DATABASE, VERSION_17, true,
Migration_16_17.INSTANCE);
- Cursor workSpecs = database.query("SELECT id FROM WorkSpec");
- assertThat(workSpecs.getCount(), is(1));
- assertThat(workSpecs.moveToFirst(), is(true));
+ Cursor workSpecs = database.query("SELECT id, input_merger_class_name FROM WorkSpec");
+ assertThat(workSpecs.getCount(), is(2));
+ assertThat(workSpecs.moveToNext(), is(true));
+ assertThat(workSpecs.getString(workSpecs.getColumnIndex("id")), is(idOne));
+ assertThat(workSpecs.getString(workSpecs.getColumnIndex("input_merger_class_name")),
+ is(OverwritingInputMerger.class.getName()));
+ assertThat(workSpecs.moveToNext(), is(true));
assertThat(workSpecs.getString(workSpecs.getColumnIndex("id")), is(idTwo));
+ assertThat(workSpecs.getString(workSpecs.getColumnIndex("input_merger_class_name")),
+ is(OverwritingInputMerger.class.getName()));
database.close();
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
index dc5093b..e36def2 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
@@ -23,6 +23,7 @@
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.work.OverwritingInputMerger
import androidx.work.impl.WorkDatabaseVersions.VERSION_1
import androidx.work.impl.WorkDatabaseVersions.VERSION_10
import androidx.work.impl.WorkDatabaseVersions.VERSION_11
@@ -299,8 +300,14 @@
object Migration_16_17 : Migration(VERSION_16, VERSION_17) {
override fun migrate(db: SupportSQLiteDatabase) {
// b/261721822: unclear how the content of input_merger_class_name could have been,
- // null such that it fails to migrate to a table with a NOT NULL constrain.
- db.execSQL("DELETE FROM WorkSpec WHERE input_merger_class_name IS NULL")
+ // null such that it fails to migrate to a table with a NOT NULL constrain, therefore
+ // set the current default value to avoid dropping the worker.
+ db.execSQL(
+ """UPDATE WorkSpec
+ SET input_merger_class_name = '${OverwritingInputMerger::class.java.name}'
+ WHERE input_merger_class_name IS NULL
+ """.trimIndent()
+ )
db.execSQL(
"""CREATE TABLE IF NOT EXISTS `_new_WorkSpec` (
`id` TEXT NOT NULL,