diff --git a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/LookaheadAnimationVisualDebugSamples.kt b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/LookaheadAnimationVisualDebugSamples.kt new file mode 100644 index 0000000000000..8ae62ac467397 --- /dev/null +++ b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/LookaheadAnimationVisualDebugSamples.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.animation.samples + +import androidx.annotation.Sampled +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.CustomizedLookaheadAnimationVisualDebugging +import androidx.compose.animation.ExperimentalLookaheadAnimationVisualDebugApi +import androidx.compose.animation.LookaheadAnimationVisualDebugging +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalLookaheadAnimationVisualDebugApi::class) +@Sampled +@Composable +fun LookaheadAnimationVisualDebuggingSample() { + var isExpanded by mutableStateOf(false) + // Wrap content with LookaheadAnimationVisualDebugging to enable visual debugging. + // Optional parameters allow color customization and decision of whether to show key labels. + // Note that enabling LookaheadAnimationVisualDebugging affects the entire UI subtree generated + // by the content lambda. It applies to all descendants, regardless of whether they are defined + // within the same lexical scope. + LookaheadAnimationVisualDebugging(isShowKeyLabelEnabled = true) { + SharedTransitionLayout(Modifier.fillMaxSize().clickable { isExpanded = !isExpanded }) { + AnimatedVisibility(visible = isExpanded) { + Box( + Modifier.offset(100.dp, 100.dp) + .size(200.dp) + .sharedElement( + rememberSharedContentState(key = "box"), + animatedVisibilityScope = this, + ) + .background(Color.Red) + ) + } + AnimatedVisibility(visible = !isExpanded) { + Box( + Modifier.offset(0.dp, 0.dp) + .size(50.dp) + .sharedElement( + rememberSharedContentState(key = "box"), + animatedVisibilityScope = this, + ) + .background(Color.Blue) + ) + } + } + } +} + +@OptIn(ExperimentalLookaheadAnimationVisualDebugApi::class) +@Sampled +@Composable +fun CustomizedLookaheadAnimationVisualDebuggingSample() { + var isExpanded by mutableStateOf(false) + // Wrap content with LookaheadAnimationVisualDebugging to enable visual debugging. + // Optional parameters allow color customization and decision of whether to show key labels. + // Note that enabling LookaheadAnimationVisualDebugging affects the entire UI subtree generated + // by the content lambda. It applies to all descendants, regardless of whether they are defined + // within the same lexical scope. + LookaheadAnimationVisualDebugging { + // Wrap content with CustomizedLookaheadAnimationVisualDebugging to customize the color of + // the bounds visualizations in the specified scope. + CustomizedLookaheadAnimationVisualDebugging(Color.Black) { + SharedTransitionLayout(Modifier.fillMaxSize().clickable { isExpanded = !isExpanded }) { + AnimatedVisibility(visible = isExpanded) { + Box( + Modifier.offset(100.dp, 100.dp) + .size(200.dp) + .sharedElement( + rememberSharedContentState(key = "box"), + animatedVisibilityScope = this, + ) + .background(Color.Red) + ) + } + AnimatedVisibility(visible = !isExpanded) { + Box( + Modifier.offset(0.dp, 0.dp) + .size(50.dp) + .sharedElement( + rememberSharedContentState(key = "box"), + animatedVisibilityScope = this, + ) + .background(Color.Blue) + ) + } + } + } + } +} diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LookaheadAnimationVisualDebugHelper.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LookaheadAnimationVisualDebugHelper.kt index c3b047132d873..0c6433a76a969 100644 --- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LookaheadAnimationVisualDebugHelper.kt +++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LookaheadAnimationVisualDebugHelper.kt @@ -529,7 +529,10 @@ internal class LookaheadAnimationVisualDebugHelper() { } /** - * Allows enabling and customizing shared element and animated bounds animation debugging. + * Allows enabling and customizing shared element and animated bounds animation debugging. Note that + * enabling LookaheadAnimationVisualDebugging affects the entire UI subtree generated by the content + * lambda. It applies to all descendants, regardless of whether they are defined within the same + * lexical scope. * * @param isEnabled Boolean specifying whether to enable animation debugging. * @param overlayColor The color of the translucent film covering everything underneath the lifted @@ -539,6 +542,10 @@ internal class LookaheadAnimationVisualDebugHelper() { * @param isShowKeyLabelEnabled Boolean specifying whether to print animated element keys. * @param content The composable content that debugging visualizations will apply to, although which * visualizations appear depends on where the Modifiers are placed. + * + * An example of how to use it: + * + * @sample androidx.compose.animation.samples.LookaheadAnimationVisualDebuggingSample */ @Composable @ExperimentalLookaheadAnimationVisualDebugApi @@ -564,11 +571,18 @@ public fun LookaheadAnimationVisualDebugging( } /** - * Allows customizing a particular shared element or animated bounds animation for debugging. + * Allows customizing a particular shared element or animated bounds animation for debugging. Note + * that enabling CustomizedLookaheadAnimationVisualDebugging affects the entire UI subtree generated + * by the content lambda. It applies to all descendants, regardless of whether they are defined + * within the same lexical scope. * * @param debugColor The custom color specified for animation debugging visualizations. * @param content The composable content that debugging visualizations will apply to, although which * visualizations appear depends on where the Modifiers are placed. + * + * An example of how to use it: + * + * @sample androidx.compose.animation.samples.CustomizedLookaheadAnimationVisualDebuggingSample */ @Composable @ExperimentalLookaheadAnimationVisualDebugApi diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt index af2309471668b..c77bbea780a95 100644 --- a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt +++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt @@ -18,13 +18,26 @@ package androidx.compose.remote.player.compose import androidx.annotation.RestrictTo +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.size import androidx.compose.remote.core.CoreDocument -import androidx.compose.remote.player.compose.impl.RemoteDocumentViewPlayer +import androidx.compose.remote.core.operations.Theme +import androidx.compose.remote.player.core.RemoteDocument +import androidx.compose.remote.player.core.action.NamedActionHandler +import androidx.compose.remote.player.core.action.StateUpdaterActionCallback import androidx.compose.remote.player.core.platform.BitmapLoader import androidx.compose.remote.player.core.state.StateUpdater import androidx.compose.remote.player.view.RemoteComposePlayer import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView /** A player of a [CoreDocument] */ @Composable @@ -40,16 +53,64 @@ public fun RemoteDocumentPlayer( onNamedAction: (name: String, value: Any?, stateUpdater: StateUpdater) -> Unit = { _, _, _ -> }, bitmapLoader: BitmapLoader? = null, ) { - RemoteDocumentViewPlayer( - document = document, - documentWidth = documentWidth, - documentHeight = documentHeight, - modifier = modifier, - debugMode = debugMode, - init = init, - update = update, - onAction = onAction, - onNamedAction = onNamedAction, - bitmapLoader = bitmapLoader, + var inDarkTheme by remember { mutableStateOf(false) } + var playbackTheme by remember { mutableIntStateOf(Theme.UNSPECIFIED) } + + val remoteDoc = remember(document) { RemoteDocument(document) } + + inDarkTheme = + when (AppCompatDelegate.getDefaultNightMode()) { + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> isSystemInDarkTheme() + AppCompatDelegate.MODE_NIGHT_YES -> true + AppCompatDelegate.MODE_NIGHT_NO -> false + AppCompatDelegate.MODE_NIGHT_UNSPECIFIED -> isSystemInDarkTheme() + else -> { + false + } + } + + playbackTheme = + if (inDarkTheme) { + Theme.DARK + } else { + Theme.LIGHT + } + + AndroidView( + modifier = modifier.size(documentWidth.dp, documentHeight.dp), + factory = { + RemoteComposePlayer(it).apply { + init(this) + if (bitmapLoader != null) { + setBitmapLoader(bitmapLoader) + } + } + }, + update = { remoteComposePlayer -> + remoteComposePlayer.setTheme(playbackTheme) + remoteComposePlayer.setDocument(remoteDoc) + remoteComposePlayer.setDebug(debugMode) + remoteComposePlayer.document.document.clearActionCallbacks() + remoteComposePlayer.document.document.addIdActionListener { id, value -> + onAction.invoke(id, value) + } + remoteComposePlayer.document.document.addActionCallback( + object : + StateUpdaterActionCallback( + remoteComposePlayer.stateUpdater, + object : NamedActionHandler { + override fun execute( + name: String, + value: Any?, + stateUpdater: StateUpdater, + ) { + onNamedAction.invoke(name, value, stateUpdater) + } + }, + ) {} + ) + // use + update(remoteComposePlayer) + }, ) } diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/impl/RemoteDocumentViewPlayer.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/impl/RemoteDocumentViewPlayer.kt deleted file mode 100644 index 7708ed1e207d7..0000000000000 --- a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/impl/RemoteDocumentViewPlayer.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.compose.remote.player.compose.impl - -import androidx.annotation.RestrictTo -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.size -import androidx.compose.remote.core.CoreDocument -import androidx.compose.remote.core.operations.Theme -import androidx.compose.remote.player.core.RemoteDocument -import androidx.compose.remote.player.core.action.NamedActionHandler -import androidx.compose.remote.player.core.action.StateUpdaterActionCallback -import androidx.compose.remote.player.core.platform.BitmapLoader -import androidx.compose.remote.player.core.state.StateUpdater -import androidx.compose.remote.player.view.RemoteComposePlayer -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -@Composable -internal fun RemoteDocumentViewPlayer( - document: CoreDocument, - documentWidth: Int, - documentHeight: Int, - debugMode: Int, - modifier: Modifier = Modifier, - init: (RemoteComposePlayer) -> Unit = {}, - update: (RemoteComposePlayer) -> Unit = {}, - onAction: (actionId: Int, value: String?) -> Unit = { _, _ -> }, - onNamedAction: (name: String, value: Any?, stateUpdater: StateUpdater) -> Unit = { _, _, _ -> }, - bitmapLoader: BitmapLoader? = null, -) { - var inDarkTheme by remember { mutableStateOf(false) } - var playbackTheme by remember { mutableIntStateOf(Theme.UNSPECIFIED) } - - val remoteDoc = remember(document) { RemoteDocument(document) } - - inDarkTheme = - when (AppCompatDelegate.getDefaultNightMode()) { - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> isSystemInDarkTheme() - AppCompatDelegate.MODE_NIGHT_YES -> true - AppCompatDelegate.MODE_NIGHT_NO -> false - AppCompatDelegate.MODE_NIGHT_UNSPECIFIED -> isSystemInDarkTheme() - else -> { - false - } - } - - playbackTheme = - if (inDarkTheme) { - Theme.DARK - } else { - Theme.LIGHT - } - - AndroidView( - modifier = modifier.size(documentWidth.dp, documentHeight.dp), - factory = { - RemoteComposePlayer(it).apply { - init(this) - if (bitmapLoader != null) { - setBitmapLoader(bitmapLoader) - } - } - }, - update = { remoteComposePlayer -> - remoteComposePlayer.setTheme(playbackTheme) - remoteComposePlayer.setDocument(remoteDoc) - remoteComposePlayer.setDebug(debugMode) - remoteComposePlayer.document.document.clearActionCallbacks() - remoteComposePlayer.document.document.addIdActionListener { id, value -> - onAction.invoke(id, value) - } - remoteComposePlayer.document.document.addActionCallback( - object : - StateUpdaterActionCallback( - remoteComposePlayer.stateUpdater, - object : NamedActionHandler { - override fun execute( - name: String, - value: Any?, - stateUpdater: StateUpdater, - ) { - onNamedAction.invoke(name, value, stateUpdater) - } - }, - ) {} - ) - // use - update(remoteComposePlayer) - }, - ) -} diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle index 2c18d7f7c22d6..9647b9c46d0bf 100644 --- a/docs-tip-of-tree/build.gradle +++ b/docs-tip-of-tree/build.gradle @@ -313,6 +313,7 @@ dependencies { docs(project(":resourceinspection:resourceinspection-annotation")) kmpDocs(project(":room3:room3-common")) docs(project(":room3:room3-guava")) + docs(project(":room3:room3-livedata")) kmpDocs(project(":room3:room3-migration")) kmpDocs(project(":room3:room3-paging")) docs(project(":room3:room3-paging-guava")) diff --git a/room3/integration-tests/kotlintestapp/build.gradle b/room3/integration-tests/kotlintestapp/build.gradle index 670134ecdbb62..ed735e6d78e74 100644 --- a/room3/integration-tests/kotlintestapp/build.gradle +++ b/room3/integration-tests/kotlintestapp/build.gradle @@ -62,6 +62,7 @@ tasks.withType(KotlinCompilationTask).configureEach { dependencies { implementation(project(":room3:room3-common")) implementation(project(":room3:room3-runtime")) + implementation(project(":room3:room3-livedata")) implementation(project(":room3:room3-paging")) implementation(project(":room3:room3-sqlite-wrapper")) implementation(project(":paging:paging-runtime")) diff --git a/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/TestDatabase.kt b/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/TestDatabase.kt index 3a638ee404f94..6ebb65a4edbfa 100644 --- a/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/TestDatabase.kt +++ b/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/TestDatabase.kt @@ -16,6 +16,7 @@ package androidx.room3.integration.kotlintestapp +import androidx.room3.DaoReturnTypeConverters import androidx.room3.Database import androidx.room3.RoomDatabase import androidx.room3.TypeConverter @@ -57,6 +58,7 @@ import androidx.room3.integration.kotlintestapp.vo.School import androidx.room3.integration.kotlintestapp.vo.Song import androidx.room3.integration.kotlintestapp.vo.Toy import androidx.room3.integration.kotlintestapp.vo.User +import androidx.room3.livedata.LiveDataDaoReturnTypeConverter import java.nio.ByteBuffer import java.util.Date import java.util.UUID @@ -91,6 +93,7 @@ import java.util.UUID version = 1, exportSchema = false, ) +@DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class) @TypeConverters(TestDatabase.Converters::class) abstract class TestDatabase : RoomDatabase() { diff --git a/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt b/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt index 3561b6870ef78..b3f7ee1cdb80c 100644 --- a/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt +++ b/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt @@ -20,6 +20,7 @@ import androidx.kruth.assertThat import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow import androidx.room3.Dao +import androidx.room3.DaoReturnTypeConverters import androidx.room3.Database import androidx.room3.Entity import androidx.room3.Insert @@ -28,6 +29,7 @@ import androidx.room3.Query import androidx.room3.Room import androidx.room3.RoomDatabase import androidx.room3.integration.kotlintestapp.assumeKsp +import androidx.room3.livedata.LiveDataDaoReturnTypeConverter import androidx.sqlite.driver.AndroidSQLiteDriver import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -187,6 +189,7 @@ class BoxedNonNullTypesTest { version = 1, exportSchema = false, ) + @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class) abstract class MyDb : RoomDatabase() { abstract fun myDao(): MyDao } diff --git a/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt b/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt index 9e48643f33730..c2d7f238f8e03 100644 --- a/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt +++ b/room3/integration-tests/kotlintestapp/src/androidTest/java/androidx/room3/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt @@ -19,6 +19,7 @@ import android.content.Context import androidx.kruth.assertThat import androidx.kruth.assertWithMessage import androidx.room3.Dao +import androidx.room3.DaoReturnTypeConverters import androidx.room3.Database import androidx.room3.Entity import androidx.room3.Insert @@ -38,6 +39,7 @@ import androidx.room3.integration.kotlintestapp.vo.PetUser import androidx.room3.integration.kotlintestapp.vo.PetWithUser import androidx.room3.integration.kotlintestapp.vo.Robot import androidx.room3.integration.kotlintestapp.vo.Toy +import androidx.room3.livedata.LiveDataDaoReturnTypeConverter import androidx.sqlite.driver.AndroidSQLiteDriver import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -153,6 +155,7 @@ class ProvidedTypeConverterTest { version = 1, exportSchema = false, ) + @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class) @TypeConverters(TimeStampConverter::class, UUIDConverter::class) internal abstract class TestDatabaseWithConverterOne : RoomDatabase() { abstract fun petDao(): PetDao diff --git a/room3/room3-compiler/build.gradle b/room3/room3-compiler/build.gradle index 2f7cf2cafbc65..3808f06df9bf9 100644 --- a/room3/room3-compiler/build.gradle +++ b/room3/room3-compiler/build.gradle @@ -87,6 +87,7 @@ dependencies { testImplementation(libs.antlr4) testImplementation(SdkHelperKt.getSdkDependency(project)) testImplementationAarAsJar(project(":room3:room3-runtime")) + testImplementationAarAsJar(project(":room3:room3-livedata")) testImplementationAarAsJar(project(":sqlite:sqlite")) testImplementation(project(":internal-testutils-common")) } diff --git a/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/TypeAdapterStore.kt b/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/TypeAdapterStore.kt index 5536ffc6b69f2..d85c02dd549cb 100644 --- a/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/TypeAdapterStore.kt +++ b/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/TypeAdapterStore.kt @@ -51,7 +51,6 @@ import androidx.room3.solver.binderprovider.DaoReturnTypeQueryResultBinderProvid import androidx.room3.solver.binderprovider.GuavaListenableFutureQueryResultBinderProvider import androidx.room3.solver.binderprovider.InstantQueryResultBinderProvider import androidx.room3.solver.binderprovider.ListenableFuturePagingSourceQueryResultBinderProvider -import androidx.room3.solver.binderprovider.LiveDataQueryResultBinderProvider import androidx.room3.solver.binderprovider.PagingSourceQueryResultBinderProvider import androidx.room3.solver.binderprovider.RxJava3PagingSourceQueryResultBinderProvider import androidx.room3.solver.binderprovider.RxLambdaQueryResultBinderProvider @@ -221,7 +220,6 @@ private constructor( private val queryResultBinderProviders: List = mutableListOf().apply { - add(LiveDataQueryResultBinderProvider(context)) add(GuavaListenableFutureQueryResultBinderProvider(context)) addAll(RxQueryResultBinderProvider.getAll(context)) addAll(RxLambdaQueryResultBinderProvider.getAll(context)) diff --git a/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/binderprovider/LiveDataQueryResultBinderProvider.kt b/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/binderprovider/LiveDataQueryResultBinderProvider.kt deleted file mode 100644 index fa36d7a1db8b8..0000000000000 --- a/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/binderprovider/LiveDataQueryResultBinderProvider.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.room3.solver.binderprovider - -import androidx.room3.compiler.processing.XRawType -import androidx.room3.compiler.processing.XType -import androidx.room3.ext.LifecyclesTypeNames -import androidx.room3.processor.Context -import androidx.room3.solver.ObservableQueryResultBinderProvider -import androidx.room3.solver.query.result.LiveDataQueryResultBinder -import androidx.room3.solver.query.result.QueryResultAdapter -import androidx.room3.solver.query.result.QueryResultBinder - -class LiveDataQueryResultBinderProvider(context: Context) : - ObservableQueryResultBinderProvider(context) { - private val liveDataType: XRawType? by lazy { - context.processingEnv.findType(LifecyclesTypeNames.LIVE_DATA.canonicalName)?.rawType - } - - override fun extractTypeArg(declared: XType): XType = declared.typeArguments.first() - - override fun create( - typeArg: XType, - resultAdapter: QueryResultAdapter?, - tableNames: Set, - ): QueryResultBinder { - return LiveDataQueryResultBinder( - typeArg = typeArg, - tableNames = tableNames, - adapter = resultAdapter, - ) - } - - override fun matches(declared: XType): Boolean = - declared.typeArguments.size == 1 && isLiveData(declared) - - private fun isLiveData(declared: XType): Boolean { - if (liveDataType == null) { - return false - } - return declared.rawType.isAssignableFrom(liveDataType!!) - } -} diff --git a/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/query/result/DaoReturnTypeQueryResultBinder.kt b/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/query/result/DaoReturnTypeQueryResultBinder.kt index 569535d7d93ef..46bec0b2774e0 100644 --- a/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/query/result/DaoReturnTypeQueryResultBinder.kt +++ b/room3/room3-compiler/src/main/kotlin/androidx/room3/solver/query/result/DaoReturnTypeQueryResultBinder.kt @@ -26,7 +26,6 @@ import androidx.room3.ext.ArrayLiteral import androidx.room3.ext.CommonTypeNames import androidx.room3.ext.InvokeWithLambdaParameter import androidx.room3.ext.LambdaSpec -import androidx.room3.ext.RoomMemberNames.DB_UTIL_PERFORM_BLOCKING import androidx.room3.ext.RoomMemberNames.DB_UTIL_PERFORM_SUSPENDING import androidx.room3.ext.SQLiteDriverTypeNames import androidx.room3.solver.CodeGenScope @@ -70,9 +69,7 @@ class DaoReturnTypeQueryResultBinder( val performBlock = InvokeWithLambdaParameter( scope = scope, - functionName = - if (converter.isSuspend) DB_UTIL_PERFORM_SUSPENDING - else DB_UTIL_PERFORM_BLOCKING, + functionName = DB_UTIL_PERFORM_SUSPENDING, argFormat = listOf("%N", "%L", "%L"), args = listOf(dbProperty, /* isReadOnly= */ true, inTransaction), diff --git a/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/QueryFunctionProcessorTest.kt b/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/QueryFunctionProcessorTest.kt index d8372d6bac3d3..5a1c6986d4010 100644 --- a/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/QueryFunctionProcessorTest.kt +++ b/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/QueryFunctionProcessorTest.kt @@ -43,7 +43,6 @@ import androidx.room3.processor.ProcessorErrors.cannotFindQueryResultAdapter import androidx.room3.processor.ProcessorErrors.mayNeedMapColumn import androidx.room3.solver.query.result.DataClassRowAdapter import androidx.room3.solver.query.result.ListQueryResultAdapter -import androidx.room3.solver.query.result.LiveDataQueryResultBinder import androidx.room3.solver.query.result.SingleColumnRowAdapter import androidx.room3.solver.query.result.SingleItemQueryResultAdapter import androidx.room3.testing.context @@ -53,8 +52,10 @@ import androidx.room3.vo.ReadQueryFunction import androidx.room3.vo.Warning import androidx.room3.vo.WriteQueryFunction import createVerifierFromEntitiesAndViews +import kotlin.collections.listOf import mockElementAndType import org.junit.AssumptionViolatedException +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -333,6 +334,8 @@ class QueryFunctionProcessorTest(private val enableVerification: Boolean) { } @Test + @Ignore("b/482435352") // Temporarily ignore, Java sources are not picking up the + // livedata dependency from the aar fun testLiveDataWithWithClause() { singleQueryMethod( """ @@ -349,6 +352,8 @@ class QueryFunctionProcessorTest(private val enableVerification: Boolean) { } @Test + @Ignore("b/482435352") // Temporarily ignore, Java sources are not picking up the + // livedata dependency from the aar fun testLiveDataWithNothingToObserve() { singleQueryMethod( """ @@ -363,6 +368,8 @@ class QueryFunctionProcessorTest(private val enableVerification: Boolean) { } @Test + @Ignore("b/482435352") // Temporarily ignore, Java sources are not picking up the + // livedata dependency from the aar fun testLiveDataWithWithClauseAndNothingToObserve() { singleQueryMethod( """ @@ -529,24 +536,6 @@ class QueryFunctionProcessorTest(private val enableVerification: Boolean) { } } - @Test - fun testLiveDataQuery() { - singleQueryMethod( - """ - @Query("select name from user where uid = :id") - abstract ${LifecyclesTypeNames.LIVE_DATA.canonicalName} nameLiveData(String id); - """ - ) { parsedQuery, _ -> - assertThat(parsedQuery.returnType.asTypeName()) - .isEqualTo( - LifecyclesTypeNames.LIVE_DATA.parametrizedBy(STRING.copy(nullable = true)) - .copy(nullable = true) - ) - - assertThat(parsedQuery.queryResultBinder).isInstanceOf() - } - } - @Test fun testBadReturnForDeleteQuery() { singleQueryMethod( diff --git a/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/RawQueryFunctionProcessorTest.kt b/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/RawQueryFunctionProcessorTest.kt index 2d49d326497ef..57fb993215239 100644 --- a/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/RawQueryFunctionProcessorTest.kt +++ b/room3/room3-compiler/src/test/kotlin/androidx/room3/processor/RawQueryFunctionProcessorTest.kt @@ -37,6 +37,7 @@ import androidx.room3.testing.context import androidx.room3.vo.RawQueryFunction import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat +import org.junit.Ignore import org.junit.Test class RawQueryFunctionProcessorTest { @@ -105,6 +106,7 @@ class RawQueryFunctionProcessorTest { } @Test + @Ignore("b/482435784") fun observableWithoutEntities() { singleQueryMethod( """ diff --git a/room3/room3-compiler/src/test/kotlin/androidx/room3/solver/TypeAdapterStoreTest.kt b/room3/room3-compiler/src/test/kotlin/androidx/room3/solver/TypeAdapterStoreTest.kt index 28e237227b8fb..a08eec3f27fef 100644 --- a/room3/room3-compiler/src/test/kotlin/androidx/room3/solver/TypeAdapterStoreTest.kt +++ b/room3/room3-compiler/src/test/kotlin/androidx/room3/solver/TypeAdapterStoreTest.kt @@ -34,7 +34,6 @@ import androidx.room3.compiler.processing.util.runKspTest import androidx.room3.compiler.processing.util.runProcessorTest import androidx.room3.ext.CommonTypeNames import androidx.room3.ext.GuavaUtilConcurrentTypeNames -import androidx.room3.ext.LifecyclesTypeNames import androidx.room3.ext.PagingTypeNames import androidx.room3.ext.ReactiveStreamsTypeNames import androidx.room3.ext.RoomTypeNames.ROOM_DB @@ -48,7 +47,6 @@ import androidx.room3.processor.DaoProcessor import androidx.room3.processor.DaoProcessorTest import androidx.room3.processor.ProcessorErrors import androidx.room3.solver.binderprovider.ListenableFuturePagingSourceQueryResultBinderProvider -import androidx.room3.solver.binderprovider.LiveDataQueryResultBinderProvider import androidx.room3.solver.binderprovider.PagingSourceQueryResultBinderProvider import androidx.room3.solver.binderprovider.RxJava3PagingSourceQueryResultBinderProvider import androidx.room3.solver.binderprovider.RxQueryResultBinderProvider @@ -984,19 +982,6 @@ class TypeAdapterStoreTest { } } - @Test - fun testFindLiveData() { - runKspTest(sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) { invocation -> - val liveData = - invocation.processingEnv.requireTypeElement(LifecyclesTypeNames.LIVE_DATA) - assertThat(liveData, notNullValue()) - assertThat( - LiveDataQueryResultBinderProvider(invocation.context).matches(liveData.type), - `is`(true), - ) - } - } - @Test fun findPagingSourceIntKey() { runKspTest(sources = listOf(COMMON.LIMIT_OFFSET_PAGING_SOURCE)) { invocation -> diff --git a/room3/room3-compiler/src/test/kotlin/androidx/room3/testing/test_util.kt b/room3/room3-compiler/src/test/kotlin/androidx/room3/testing/test_util.kt index 482c359bca60c..443c270148e18 100644 --- a/room3/room3-compiler/src/test/kotlin/androidx/room3/testing/test_util.kt +++ b/room3/room3-compiler/src/test/kotlin/androidx/room3/testing/test_util.kt @@ -81,6 +81,7 @@ object COMMON { val LIVE_DATA by lazy { loadJavaCode("common/input/LiveData.java", LifecyclesTypeNames.LIVE_DATA.canonicalName) } + val FLOW_LIVE_DATA by lazy { loadKotlinCode("common/input/FlowLiveData.kt") } val COMPUTABLE_LIVE_DATA by lazy { loadJavaCode( "common/input/ComputableLiveData.java", diff --git a/room3/room3-compiler/src/test/kotlin/androidx/room3/writer/DaoKotlinCodeGenTest.kt b/room3/room3-compiler/src/test/kotlin/androidx/room3/writer/DaoKotlinCodeGenTest.kt index e65670076b5be..64c88c7744ded 100644 --- a/room3/room3-compiler/src/test/kotlin/androidx/room3/writer/DaoKotlinCodeGenTest.kt +++ b/room3/room3-compiler/src/test/kotlin/androidx/room3/writer/DaoKotlinCodeGenTest.kt @@ -2726,8 +2726,13 @@ class DaoKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() { """ import androidx.room3.* import androidx.lifecycle.* + import kotlinx.coroutines.flow.map + import androidx.lifecycle.LiveData + import androidx.lifecycle.asLiveData + import androidx.room3.livedata.LiveDataDaoReturnTypeConverter @Dao + @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class) interface MyDao { @Query("SELECT * FROM MyEntity WHERE pk IN (:arg)") fun getLiveData(vararg arg: String?): LiveData @@ -2748,7 +2753,7 @@ class DaoKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() { runTest( sources = listOf(src, databaseSrc), expectedFilePath = getTestGoldenPath(testName.methodName), - compiledFiles = compileFiles(listOf(COMMON.LIVE_DATA)), + compiledFiles = compileFiles(listOf(COMMON.LIVE_DATA, COMMON.FLOW_LIVE_DATA)), ) } diff --git a/room3/room3-compiler/src/test/test-data/common/input/FlowLiveData.kt b/room3/room3-compiler/src/test/test-data/common/input/FlowLiveData.kt new file mode 100644 index 0000000000000..76df5bafa91df --- /dev/null +++ b/room3/room3-compiler/src/test/test-data/common/input/FlowLiveData.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("FlowLiveData") +package androidx.lifecycle + +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.flow.Flow + +public fun Flow.asLiveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = 5000L, +): LiveData { + TODO() +} \ No newline at end of file diff --git a/room3/room3-compiler/src/test/test-data/kotlinCodeGen/customReadDaoReturnType.kt b/room3/room3-compiler/src/test/test-data/kotlinCodeGen/customReadDaoReturnType.kt index f04cb71b81a27..ea605a11e4430 100644 --- a/room3/room3-compiler/src/test/test-data/kotlinCodeGen/customReadDaoReturnType.kt +++ b/room3/room3-compiler/src/test/test-data/kotlinCodeGen/customReadDaoReturnType.kt @@ -1,6 +1,5 @@ import androidx.room3.RoomDatabase import androidx.room3.util.getColumnIndexOrThrow -import androidx.room3.util.performBlocking import androidx.room3.util.performSuspending import androidx.sqlite.SQLiteStatement import androidx.sqlite.step @@ -74,7 +73,7 @@ internal class MyDao_Impl( public override fun getBlockingFooList(): Foo> { val _sql: String = "SELECT * FROM MyEntity" return __fooReturnTypeConverter.convertBlocking(__db, arrayOf("MyEntity")) { - performBlocking(__db, true, false) { _connection -> + performSuspending(__db, true, false) { _connection -> val _stmt: SQLiteStatement = _connection.prepare(_sql) try { val _columnIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk") diff --git a/room3/room3-compiler/src/test/test-data/kotlinCodeGen/liveDataCallable.kt b/room3/room3-compiler/src/test/test-data/kotlinCodeGen/liveDataCallable.kt index c03009c463291..b3fa16c4ded2c 100644 --- a/room3/room3-compiler/src/test/test-data/kotlinCodeGen/liveDataCallable.kt +++ b/room3/room3-compiler/src/test/test-data/kotlinCodeGen/liveDataCallable.kt @@ -1,9 +1,10 @@ import androidx.lifecycle.LiveData import androidx.room3.RoomDatabase +import androidx.room3.livedata.LiveDataDaoReturnTypeConverter import androidx.room3.util.appendPlaceholders import androidx.room3.util.getColumnIndexOrThrow +import androidx.room3.util.performSuspending import androidx.sqlite.SQLiteStatement -import androidx.sqlite.prepare import androidx.sqlite.step import javax.`annotation`.processing.Generated import kotlin.Int @@ -19,6 +20,9 @@ internal class MyDao_Impl( __db: RoomDatabase, ) : MyDao { private val __db: RoomDatabase + + private val __liveDataDaoReturnTypeConverter: LiveDataDaoReturnTypeConverter = + LiveDataDaoReturnTypeConverter() init { this.__db = __db } @@ -30,33 +34,35 @@ internal class MyDao_Impl( appendPlaceholders(_stringBuilder, _inputSize) _stringBuilder.append(")") val _sql: String = _stringBuilder.toString() - return __db.invalidationTracker.createLiveData(arrayOf("MyEntity"), false) { _connection -> - val _stmt: SQLiteStatement = _connection.prepare(_sql) - try { - var _argIndex: Int = 1 - for (_item: String? in arg) { - if (_item == null) { - _stmt.bindNull(_argIndex) + return __liveDataDaoReturnTypeConverter.convert(__db, arrayOf("MyEntity")) { + performSuspending(__db, true, false) { _connection -> + val _stmt: SQLiteStatement = _connection.prepare(_sql) + try { + var _argIndex: Int = 1 + for (_item: String? in arg) { + if (_item == null) { + _stmt.bindNull(_argIndex) + } else { + _stmt.bindText(_argIndex, _item) + } + _argIndex++ + } + val _columnIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk") + val _columnIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other") + val _result: MyEntity + if (_stmt.step()) { + val _tmpPk: Int + _tmpPk = _stmt.getLong(_columnIndexOfPk).toInt() + val _tmpOther: String + _tmpOther = _stmt.getText(_columnIndexOfOther) + _result = MyEntity(_tmpPk,_tmpOther) } else { - _stmt.bindText(_argIndex, _item) + error("The query result was empty, but expected a single row to return a NON-NULL object of type 'MyEntity'.") } - _argIndex++ - } - val _columnIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk") - val _columnIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other") - val _result: MyEntity - if (_stmt.step()) { - val _tmpPk: Int - _tmpPk = _stmt.getLong(_columnIndexOfPk).toInt() - val _tmpOther: String - _tmpOther = _stmt.getText(_columnIndexOfOther) - _result = MyEntity(_tmpPk,_tmpOther) - } else { - error("The query result was empty, but expected a single row to return a NON-NULL object of type 'MyEntity'.") + _result + } finally { + _stmt.close() } - _result - } finally { - _stmt.close() } } } @@ -68,33 +74,35 @@ internal class MyDao_Impl( appendPlaceholders(_stringBuilder, _inputSize) _stringBuilder.append(")") val _sql: String = _stringBuilder.toString() - return __db.invalidationTracker.createLiveData(arrayOf("MyEntity"), false) { _connection -> - val _stmt: SQLiteStatement = _connection.prepare(_sql) - try { - var _argIndex: Int = 1 - for (_item: String? in arg) { - if (_item == null) { - _stmt.bindNull(_argIndex) + return __liveDataDaoReturnTypeConverter.convert(__db, arrayOf("MyEntity")) { + performSuspending(__db, true, false) { _connection -> + val _stmt: SQLiteStatement = _connection.prepare(_sql) + try { + var _argIndex: Int = 1 + for (_item: String? in arg) { + if (_item == null) { + _stmt.bindNull(_argIndex) + } else { + _stmt.bindText(_argIndex, _item) + } + _argIndex++ + } + val _columnIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk") + val _columnIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other") + val _result: MyEntity? + if (_stmt.step()) { + val _tmpPk: Int + _tmpPk = _stmt.getLong(_columnIndexOfPk).toInt() + val _tmpOther: String + _tmpOther = _stmt.getText(_columnIndexOfOther) + _result = MyEntity(_tmpPk,_tmpOther) } else { - _stmt.bindText(_argIndex, _item) + _result = null } - _argIndex++ - } - val _columnIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk") - val _columnIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other") - val _result: MyEntity? - if (_stmt.step()) { - val _tmpPk: Int - _tmpPk = _stmt.getLong(_columnIndexOfPk).toInt() - val _tmpOther: String - _tmpOther = _stmt.getText(_columnIndexOfOther) - _result = MyEntity(_tmpPk,_tmpOther) - } else { - _result = null + _result + } finally { + _stmt.close() } - _result - } finally { - _stmt.close() } } } diff --git a/room3/room3-livedata/api/current.txt b/room3/room3-livedata/api/current.txt new file mode 100644 index 0000000000000..567be9a8dda4a --- /dev/null +++ b/room3/room3-livedata/api/current.txt @@ -0,0 +1,10 @@ +// Signature format: 4.0 +package androidx.room3.livedata { + + public final class LiveDataDaoReturnTypeConverter { + ctor public LiveDataDaoReturnTypeConverter(); + method @androidx.room3.DaoReturnTypeConverter public androidx.lifecycle.LiveData convert(androidx.room3.RoomDatabase database, String[] tableNames, kotlin.jvm.functions.Function1,? extends java.lang.Object?> executeAndConvert); + } + +} + diff --git a/room3/room3-livedata/api/res-current.txt b/room3/room3-livedata/api/res-current.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/room3/room3-livedata/api/restricted_current.txt b/room3/room3-livedata/api/restricted_current.txt new file mode 100644 index 0000000000000..567be9a8dda4a --- /dev/null +++ b/room3/room3-livedata/api/restricted_current.txt @@ -0,0 +1,10 @@ +// Signature format: 4.0 +package androidx.room3.livedata { + + public final class LiveDataDaoReturnTypeConverter { + ctor public LiveDataDaoReturnTypeConverter(); + method @androidx.room3.DaoReturnTypeConverter public androidx.lifecycle.LiveData convert(androidx.room3.RoomDatabase database, String[] tableNames, kotlin.jvm.functions.Function1,? extends java.lang.Object?> executeAndConvert); + } + +} + diff --git a/room3/room3-livedata/build.gradle b/room3/room3-livedata/build.gradle new file mode 100644 index 0000000000000..4b3692db0ace4 --- /dev/null +++ b/room3/room3-livedata/build.gradle @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.build.KotlinTarget +import androidx.build.SoftwareType + +plugins { + id("AndroidXPlugin") + id("com.android.library") +} + +dependencies { + api(project(":room3:room3-common")) + api(project(":room3:room3-runtime")) + api("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") +} + +androidx { + name = "Room LiveData Extension" + type = SoftwareType.PUBLISHED_LIBRARY + inceptionYear = "2026" + description = "Contains the LiveData DAO return type converter for Room." + kotlinTarget = KotlinTarget.KOTLIN_2_1 +} + +android { + namespace = "androidx.room3.livedata" +} diff --git a/room3/room3-livedata/src/main/kotlin/androidx.room3.livedata/LiveDataDaoReturnTypeConverter.kt b/room3/room3-livedata/src/main/kotlin/androidx.room3.livedata/LiveDataDaoReturnTypeConverter.kt new file mode 100644 index 0000000000000..00907c3cc6415 --- /dev/null +++ b/room3/room3-livedata/src/main/kotlin/androidx.room3.livedata/LiveDataDaoReturnTypeConverter.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room3.livedata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import androidx.room3.DaoReturnTypeConverter +import androidx.room3.RoomDatabase +import kotlinx.coroutines.flow.map + +/** + * A [DaoReturnTypeConverter] container that allows Room to return [LiveData] from [Dao] functions. + */ +public class LiveDataDaoReturnTypeConverter { + /** + * This [convert] function will be called from Room generated code to convert a Room query + * result to the return type of this function. + * + * The returned [LiveData] is backed by a [kotlinx.coroutines.flow.Flow] and created via the + * [asLiveData] API on Room's [kotlinx.coroutines.flow.Flow] returned by the + * [androidx.room3.InvalidationTracker.createFlow]. It inherits the default timeout behavior (5 + * seconds) where the upstream Flow is cancelled if the LiveData becomes inactive. + * + * @param database RoomDatabase instance + * @param tableNames List of names of the tables of the RoomDatabase + * @param executeAndConvert A suspend lambda function that invokes the part of the generated + * code that executes the query. + */ + @DaoReturnTypeConverter + public fun convert( + database: RoomDatabase, + tableNames: Array, + executeAndConvert: suspend () -> T, + ): LiveData { + return database.invalidationTracker + .createFlow(*tableNames) + .map { _ -> executeAndConvert.invoke() } + .asLiveData(database.getQueryContext()) + } +} diff --git a/room3/room3-runtime/api/restricted_current.txt b/room3/room3-runtime/api/restricted_current.txt index 3301fecb72b76..e5bc169dba056 100644 --- a/room3/room3-runtime/api/restricted_current.txt +++ b/room3/room3-runtime/api/restricted_current.txt @@ -89,7 +89,6 @@ package androidx.room3 { method public final kotlinx.coroutines.flow.Flow> createFlow(java.lang.String... tables); method public final kotlinx.coroutines.flow.Flow> createFlow(String[] tables, optional boolean emitInitialState); method @BytecodeOnly public static kotlinx.coroutines.flow.Flow! createFlow$default(androidx.room3.InvalidationTracker!, String![]!, boolean, int, Object!); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final androidx.lifecycle.LiveData createLiveData(String[] tableNames, boolean inTransaction, kotlin.jvm.functions.Function2,? extends java.lang.Object?> computeFunction); method public final void refreshAsync(); } diff --git a/room3/room3-runtime/build.gradle b/room3/room3-runtime/build.gradle index c4cdb00be6e71..746788ff5fe65 100644 --- a/room3/room3-runtime/build.gradle +++ b/room3/room3-runtime/build.gradle @@ -130,7 +130,6 @@ androidXMultiplatform { api(libs.jspecify) api(project(":sqlite:sqlite-framework")) api(libs.kotlinCoroutinesAndroid) - compileOnly("androidx.lifecycle:lifecycle-livedata:2.9.4") implementation("androidx.annotation:annotation-experimental:1.5.0") } } @@ -141,7 +140,6 @@ androidXMultiplatform { implementation(libs.byteBuddy) implementation(libs.mockitoCore4) implementation(libs.mockitoKotlin4) - implementation("androidx.lifecycle:lifecycle-livedata-core:2.0.0") implementation(libs.testRunner) // Needed for @FlakyTest and @Ignore } } diff --git a/room3/room3-runtime/src/androidHostTest/kotlin/androidx/room3/InvalidationTrackerTest.kt b/room3/room3-runtime/src/androidHostTest/kotlin/androidx/room3/InvalidationTrackerTest.kt index d733a855305af..b5efee6fc904f 100644 --- a/room3/room3-runtime/src/androidHostTest/kotlin/androidx/room3/InvalidationTrackerTest.kt +++ b/room3/room3-runtime/src/androidHostTest/kotlin/androidx/room3/InvalidationTrackerTest.kt @@ -406,16 +406,6 @@ class InvalidationTrackerTest { .isEqualTo("There is no table with name x") } - @Test - fun createLiveDataWithNoExistingTable() { - // Validate that sending a bad createLiveData table name fails quickly - assertThrows { - tracker.createLiveData(tableNames = arrayOf("x"), inTransaction = false) {} - } - .hasMessageThat() - .isEqualTo("There is no table with name x") - } - @Test fun addAndRemoveObserver() = runTest { val invalidations = tracker.createFlow("a", emitInitialState = false).produceIn(this) diff --git a/room3/room3-runtime/src/androidMain/kotlin/androidx/room3/InvalidationTracker.android.kt b/room3/room3-runtime/src/androidMain/kotlin/androidx/room3/InvalidationTracker.android.kt index 1d7944e447f98..4170e06ea7231 100644 --- a/room3/room3-runtime/src/androidMain/kotlin/androidx/room3/InvalidationTracker.android.kt +++ b/room3/room3-runtime/src/androidMain/kotlin/androidx/room3/InvalidationTracker.android.kt @@ -18,14 +18,10 @@ package androidx.room3 import android.content.Context import android.content.Intent import androidx.annotation.RestrictTo -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData import androidx.room3.autoclose.AutoCloser import androidx.room3.concurrent.AtomicInt -import androidx.room3.util.performSuspending import androidx.sqlite.SQLiteConnection import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart @@ -217,29 +213,6 @@ actual constructor( ) } - /** - * Creates a LiveData that computes the given function once and for every other invalidation of - * the database. - * - * @param tableNames The list of tables to observe - * @param inTransaction True if the computeFunction will be done in a transaction, false - * otherwise. - * @param computeFunction The function that calculates the value - * @param T The return type - * @return A new LiveData that computes the given function when the given list of tables - * invalidates. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code - public fun createLiveData( - tableNames: Array, - inTransaction: Boolean, - computeFunction: suspend (SQLiteConnection) -> T, - ): LiveData { - return createFlow(*tableNames, emitInitialState = true) - .map { performSuspending(database, true, inTransaction, computeFunction) } - .asLiveData(database.getQueryContext()) - } - internal fun initMultiInstanceInvalidation( context: Context, name: String, diff --git a/settings.gradle b/settings.gradle index 005c7d86c1bf7..ee4cff6321709 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1039,6 +1039,7 @@ includeProject(":room3:room3-compiler-processing", [BuildType.MAIN, BuildType.CO includeProject(":room3:room3-compiler-processing-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN]) includeProject(":room3:room3-external-antlr", [BuildType.MAIN]) includeProject(":room3:room3-guava", [BuildType.MAIN]) +includeProject(":room3:room3-livedata", [BuildType.MAIN, BuildType.KMP]) includeProject(":room3:room3-gradle-plugin", [BuildType.MAIN]) includeProject(":room3:room3-migration", [BuildType.MAIN, BuildType.COMPOSE, BuildType.KMP, BuildType.INFRAROGUE]) includeProject(":room3:room3-paging", [BuildType.MAIN, BuildType.COMPOSE, BuildType.KMP, BuildType.INFRAROGUE]) diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt index 1cf6dbad51a4a..91b9f7dce8b66 100644 --- a/work/work-runtime/api/current.txt +++ b/work/work-runtime/api/current.txt @@ -27,7 +27,6 @@ package androidx.work { method @InaccessibleFromKotlin public int getMinJobSchedulerId(); method @InaccessibleFromKotlin public long getRemoteSessionTimeoutMillis(); method @InaccessibleFromKotlin public androidx.work.RunnableScheduler getRunnableScheduler(); - method @SuppressCompatibility @androidx.work.ExperimentalEventsApi public androidx.work.ScheduleEventListener? getScheduleEventListener(); method @InaccessibleFromKotlin public androidx.core.util.Consumer? getSchedulingExceptionHandler(); method @InaccessibleFromKotlin public java.util.concurrent.Executor getTaskExecutor(); method @InaccessibleFromKotlin public kotlin.coroutines.CoroutineContext getWorkerCoroutineContext(); @@ -71,7 +70,6 @@ package androidx.work { method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int loggingLevel); method public androidx.work.Configuration.Builder setRemoteSessionTimeoutMillis(@IntRange(from=0L, to=1200000L) long timeoutMillis); method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler); - method @SuppressCompatibility @androidx.work.ExperimentalEventsApi public androidx.work.Configuration.Builder setScheduleEventListener(androidx.work.ScheduleEventListener listener); method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer schedulingExceptionHandler); method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor taskExecutor); method public androidx.work.Configuration.Builder setWorkerCoroutineContext(kotlin.coroutines.CoroutineContext context); @@ -405,14 +403,6 @@ package androidx.work { method public void scheduleWithDelay(@IntRange(from=0) long, Runnable); } - @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public interface ScheduleEventListener { - method public default suspend Object? onCancelled(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onEnqueued(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onPrerequisiteFailed(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onUnblocked(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onUpdated(androidx.work.WorkInfo oldWorkInfo, androidx.work.WorkInfo updatedWorkInfo, kotlin.coroutines.Continuation); - } - public abstract class WorkContinuation { ctor public WorkContinuation(); method public static androidx.work.WorkContinuation combine(java.util.List); diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt index 1cf6dbad51a4a..91b9f7dce8b66 100644 --- a/work/work-runtime/api/restricted_current.txt +++ b/work/work-runtime/api/restricted_current.txt @@ -27,7 +27,6 @@ package androidx.work { method @InaccessibleFromKotlin public int getMinJobSchedulerId(); method @InaccessibleFromKotlin public long getRemoteSessionTimeoutMillis(); method @InaccessibleFromKotlin public androidx.work.RunnableScheduler getRunnableScheduler(); - method @SuppressCompatibility @androidx.work.ExperimentalEventsApi public androidx.work.ScheduleEventListener? getScheduleEventListener(); method @InaccessibleFromKotlin public androidx.core.util.Consumer? getSchedulingExceptionHandler(); method @InaccessibleFromKotlin public java.util.concurrent.Executor getTaskExecutor(); method @InaccessibleFromKotlin public kotlin.coroutines.CoroutineContext getWorkerCoroutineContext(); @@ -71,7 +70,6 @@ package androidx.work { method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int loggingLevel); method public androidx.work.Configuration.Builder setRemoteSessionTimeoutMillis(@IntRange(from=0L, to=1200000L) long timeoutMillis); method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler); - method @SuppressCompatibility @androidx.work.ExperimentalEventsApi public androidx.work.Configuration.Builder setScheduleEventListener(androidx.work.ScheduleEventListener listener); method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer schedulingExceptionHandler); method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor taskExecutor); method public androidx.work.Configuration.Builder setWorkerCoroutineContext(kotlin.coroutines.CoroutineContext context); @@ -405,14 +403,6 @@ package androidx.work { method public void scheduleWithDelay(@IntRange(from=0) long, Runnable); } - @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public interface ScheduleEventListener { - method public default suspend Object? onCancelled(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onEnqueued(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onPrerequisiteFailed(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onUnblocked(androidx.work.WorkInfo workInfo, kotlin.coroutines.Continuation); - method public default suspend Object? onUpdated(androidx.work.WorkInfo oldWorkInfo, androidx.work.WorkInfo updatedWorkInfo, kotlin.coroutines.Continuation); - } - public abstract class WorkContinuation { ctor public WorkContinuation(); method public static androidx.work.WorkContinuation combine(java.util.List); diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle index 5dfa5f32c4025..0cbe0d1ab5f7a 100644 --- a/work/work-runtime/build.gradle +++ b/work/work-runtime/build.gradle @@ -89,7 +89,6 @@ dependencies { androidTestImplementation(libs.testUiautomator) androidTestImplementation(libs.espressoCore) androidTestImplementation(libs.mockitoCore) - androidTestImplementation(libs.mockitoKotlin) androidTestImplementation(libs.dexmakerMockito) androidTestImplementation(project(":internal-testutils-runtime")) testImplementation(libs.junit) diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt index 358ab83bc204d..ef663fc277cf8 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt +++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt @@ -57,23 +57,16 @@ import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.clearInvocations -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) class WorkUpdateTest { val workerFactory = TrackingWorkerFactory() - val schedulingEventListener: ScheduleEventListener = mock() val testClock = TestOverrideClock() val configuration = Configuration.Builder() .setClock(testClock) .setWorkerFactory(workerFactory) .setTaskExecutor(Executors.newSingleThreadExecutor()) - .setScheduleEventListener(schedulingEventListener) .build() val env = TestEnv(configuration) val taskExecutor = env.taskExecutor @@ -310,8 +303,8 @@ class WorkUpdateTest { .setConstraints(Constraints(requiresCharging = true)) .build() val step2 = OneTimeWorkRequest.Builder(TestWorker::class).build() - workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1).await() - workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2).await() + workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1) + workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2) val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class).setId(step2.id).addTag("updated").build() assertThat(workManager.updateWork(updatedStep2).await()).isEqualTo(APPLIED_IMMEDIATELY) @@ -329,8 +322,8 @@ class WorkUpdateTest { .setConstraints(Constraints(requiresCharging = true)) .build() val step2 = OneTimeWorkRequest.Builder(TestWorker::class).build() - workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1).await() - workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2).await() + workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1) + workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2) val workInfo = workManager.getWorkInfoById(step2.id).await()!! assertThat(workInfo.state).isEqualTo(State.BLOCKED) val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class).setId(step1.id).build() @@ -615,29 +608,6 @@ class WorkUpdateTest { assertThat(workSpec.runAttemptCount).isEqualTo(1) } - @Test - @MediumTest - fun updateWork_emitsUpdateEvent() = runTest { - val oneTimeWorkRequest = - OneTimeWorkRequest.Builder(WorkerWithParam::class).setInitialDelay(10, DAYS).build() - workManager.enqueue(oneTimeWorkRequest).result.await() - clearInvocations(schedulingEventListener) - - val updatedWorkRequest = - OneTimeWorkRequest.Builder(WorkerWithParam::class).setId(oneTimeWorkRequest.id).build() - workManager.updateWork(updatedWorkRequest).await() - - val workSnapshotCaptor = argumentCaptor() - verify(schedulingEventListener, times(1)) - .onUpdated(workSnapshotCaptor.capture(), workSnapshotCaptor.capture()) - val oldWork = workSnapshotCaptor.allValues.get(0) - val newWork = workSnapshotCaptor.allValues.get(1) - assertThat(oldWork.id).isEqualTo(oneTimeWorkRequest.id) - assertThat(oldWork.generation).isEqualTo(0) - assertThat(newWork.id).isEqualTo(oneTimeWorkRequest.id) - assertThat(newWork.generation).isEqualTo(1) - } - @Test @SmallTest fun clearNextScheduleTimeOverride_incrementGeneration() = runTest { diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java index 83fb8321a55eb..119d93b8b3abc 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java +++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java @@ -97,7 +97,6 @@ import androidx.work.ListenableWorker; import androidx.work.OneTimeWorkRequest; import androidx.work.PeriodicWorkRequest; -import androidx.work.ScheduleEventListener; import androidx.work.WorkContinuation; import androidx.work.WorkInfo; import androidx.work.WorkManager; @@ -127,7 +126,6 @@ import com.google.common.util.concurrent.Futures; -import org.hamcrest.Matcher; import org.jspecify.annotations.NonNull; import org.junit.After; import org.junit.Before; @@ -157,7 +155,6 @@ public class WorkManagerImplTest { private WorkDatabase mDatabase; private Scheduler mScheduler; private WorkManagerImpl mWorkManagerImpl; - private ScheduleEventListener mSchedulingEventListener; @Rule public RepeatRule mRepeatRule = new RepeatRule(); @@ -181,13 +178,11 @@ public boolean isMainThread() { } }); mContext = ApplicationProvider.getApplicationContext(); - mSchedulingEventListener = mock(ScheduleEventListener.class); mConfiguration = new Configuration.Builder() .setExecutor(Executors.newSingleThreadExecutor()) .setClock(mClock) .setMinimumLoggingLevel(Log.DEBUG) .setWorkerFactory(spy(WorkerFactory.class)) - .setScheduleEventListener(mSchedulingEventListener) .build(); InstantWorkTaskExecutor workTaskExecutor = new InstantWorkTaskExecutor(); mWorkManagerImpl = spy(createWorkManager(mContext, mConfiguration, workTaskExecutor)); @@ -391,31 +386,6 @@ public void testEnqueue_insertWithCancelledDependencies_isStatusCancelled() assertThat(workSpecDao.getState(work2.getStringId()), is(CANCELLED)); } - @Test - @MediumTest - public void testEnqueue_blockedWork_emitsEnqueueButNoUnblockEvent() - throws ExecutionException, InterruptedException { - final int workCount = 3; - final OneTimeWorkRequest[] workArray = new OneTimeWorkRequest[workCount]; - for (int i = 0; i < workCount; ++i) { - workArray[i] = new OneTimeWorkRequest.Builder(TestWorker.class).build(); - } - mWorkManagerImpl.beginWith(workArray[0]).then(workArray[1]) - .then(workArray[2]) - .enqueue().getResult() - .get(); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(3)).onEnqueued(workSnapshotCaptor.capture(), null); - List enqueuedInfo = workSnapshotCaptor.getAllValues(); - for (int i = 0; i < workCount; ++i) { - assertThat(enqueuedInfo.get(i).getId(), is(workArray[i].getId())); - } - verify(mSchedulingEventListener, times(1)).onUnblocked(workSnapshotCaptor.capture(), null); - WorkInfo unblockedInfo = workSnapshotCaptor.getValue(); - assertThat(unblockedInfo.getId(), is(workArray[0].getId())); - } - @Test @MediumTest // TODO:(b/191892569): Investigate why this passes lintDebug with minSdkVersion = 23. @@ -879,43 +849,6 @@ public void testEnqueueUniquePeriodicWork_update() assertThat(workSpec.intervalDuration, is(MINUTES.toMillis(30))); } - @Test - @MediumTest - public void testEnqueueUniquePeriodicWork_update_emitsUpdateEvent() - throws ExecutionException, InterruptedException { - final String uniqueName = "myname"; - long enqueueTime = System.currentTimeMillis(); - PeriodicWorkRequest originalWork = new PeriodicWorkRequest.Builder( - InfiniteTestWorker.class, - 15L, - MINUTES) - .setLastEnqueueTime(enqueueTime, MILLISECONDS) - .setInitialState(ENQUEUED) - .build(); - insertNamedWorks(uniqueName, originalWork); - clearInvocations(mSchedulingEventListener); - - PeriodicWorkRequest replacementWork = new PeriodicWorkRequest.Builder( - TestWorker.class, - 30L, - MINUTES) - .build(); - mWorkManagerImpl.enqueueUniquePeriodicWork( - uniqueName, - ExistingPeriodicWorkPolicy.UPDATE, - replacementWork).getResult().get(); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(1)).onUpdated(workSnapshotCaptor.capture(), - workSnapshotCaptor.capture(), null); - WorkInfo oldWork = workSnapshotCaptor.getAllValues().get(0); - WorkInfo newWork = workSnapshotCaptor.getAllValues().get(1); - assertThat(oldWork.getId(), is(originalWork.getId())); - assertThat(oldWork.getGeneration(), is(0)); - assertThat(newWork.getId(), is(originalWork.getId())); - assertThat(newWork.getGeneration(), is(1)); - } - @Test @MediumTest public void testEnqueueUniquePeriodicWork_updateCancelled() @@ -1227,75 +1160,6 @@ public void testEnqueueUniqueWork_appendsExistingWorkOnAppend() containsInAnyOrder(appendWork1.getStringId(), appendWork2.getStringId())); } - @Test - @MediumTest - public void testInsertWithAppendWithFailedPreRequisites_emitsFailedEvents() - throws ExecutionException, InterruptedException { - when(mWorkManagerImpl.getSchedulers()).thenReturn(Collections.emptyList()); - final String uniqueName = "myname"; - OneTimeWorkRequest preRequisiteRequest = new OneTimeWorkRequest.Builder(TestWorker.class) - .build(); - - // Enqueue a prerequisite work that is later failed - mWorkManagerImpl.beginUniqueWork(uniqueName, APPEND, preRequisiteRequest) - .enqueue() - .getResult() - .get(); - WorkSpecDao workSpecDao = mDatabase.workSpecDao(); - workSpecDao.setState(FAILED, preRequisiteRequest.getStringId()); - - OneTimeWorkRequest appendRequest = new OneTimeWorkRequest.Builder(TestWorker.class) - .build(); - mWorkManagerImpl.beginUniqueWork(uniqueName, APPEND, appendRequest) - .enqueue() - .getResult() - .get(); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(2)).onEnqueued(workSnapshotCaptor.capture(), null); - List enqueuedInfo = workSnapshotCaptor.getAllValues(); - assertThat(enqueuedInfo.get(0).getId(), is(preRequisiteRequest.getId())); - assertThat(enqueuedInfo.get(1).getId(), is(appendRequest.getId())); - verify(mSchedulingEventListener, times(1)).onPrerequisiteFailed( - workSnapshotCaptor.capture(), null); - WorkInfo failedInfo = workSnapshotCaptor.getValue(); - assertThat(failedInfo.getId(), is(appendRequest.getId())); - } - - @Test - @MediumTest - public void testInsertWithAppendWithCancelledPreRequisites_emitsCancelEvents() - throws ExecutionException, InterruptedException { - when(mWorkManagerImpl.getSchedulers()).thenReturn(Collections.emptyList()); - final String uniqueName = "myname"; - OneTimeWorkRequest preRequisiteRequest = new OneTimeWorkRequest.Builder(TestWorker.class) - .build(); - - // Enqueue a prerequisite work that is later cancelled - mWorkManagerImpl.beginUniqueWork(uniqueName, APPEND, preRequisiteRequest) - .enqueue() - .getResult() - .get(); - WorkSpecDao workSpecDao = mDatabase.workSpecDao(); - workSpecDao.setState(CANCELLED, preRequisiteRequest.getStringId()); - - OneTimeWorkRequest appendRequest = new OneTimeWorkRequest.Builder(TestWorker.class) - .build(); - mWorkManagerImpl.beginUniqueWork(uniqueName, APPEND, appendRequest) - .enqueue() - .getResult() - .get(); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(2)).onEnqueued(workSnapshotCaptor.capture(), null); - List enqueuedInfo = workSnapshotCaptor.getAllValues(); - assertThat(enqueuedInfo.get(0).getId(), is(preRequisiteRequest.getId())); - assertThat(enqueuedInfo.get(1).getId(), is(appendRequest.getId())); - verify(mSchedulingEventListener, times(1)).onCancelled(workSnapshotCaptor.capture(), null); - WorkInfo cancelledInfo = workSnapshotCaptor.getValue(); - assertThat(cancelledInfo.getId(), is(appendRequest.getId())); - } - @Test @MediumTest public void testBeginUniqueWork_appendsExistingWorkToOnlyLeavesOnAppend() @@ -1863,12 +1727,6 @@ public void testCancelWorkById() throws ExecutionException, InterruptedException mWorkManagerImpl.cancelWorkById(work0.getId()).getResult().get(); assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work1.getStringId()), is(not(CANCELLED))); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(1)).onCancelled(workSnapshotCaptor.capture(), null); - WorkInfo cancelledInfo = workSnapshotCaptor.getValue(); - assertThat(cancelledInfo.getId(), is(work0.getId())); - assertThat(cancelledInfo.getState(), is(CANCELLED)); } @Test @@ -1890,14 +1748,6 @@ public void testCancelWorkById_cancelsDependentWork() assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED)); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(2)).onCancelled(workSnapshotCaptor.capture(), null); - List cancelledInfos = workSnapshotCaptor.getAllValues(); - assertThat(cancelledInfos.get(0).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(0).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(1).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(1).getState(), is(CANCELLED)); } @Test @@ -1921,12 +1771,6 @@ public void testCancelWorkById_cancelsUnfinishedWorkOnly() assertThat(workSpecDao.getState(work0.getStringId()), is(SUCCEEDED)); assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED)); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(1)).onCancelled(workSnapshotCaptor.capture(), null); - WorkInfo cancelledInfo = workSnapshotCaptor.getValue(); - assertThat(cancelledInfo.getId(), is(work1.getId())); - assertThat(cancelledInfo.getState(), is(CANCELLED)); } @Test @@ -1960,14 +1804,6 @@ public void testCancelAllWorkByTag() throws ExecutionException, InterruptedExcep assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work2.getStringId()), is(not(CANCELLED))); assertThat(workSpecDao.getState(work3.getStringId()), is(not(CANCELLED))); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(2)).onCancelled(workSnapshotCaptor.capture(), null); - List cancelledInfos = workSnapshotCaptor.getAllValues(); - assertThat(cancelledInfos.get(0).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(0).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(1).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(1).getState(), is(CANCELLED)); } @Test @@ -2015,20 +1851,6 @@ public void testCancelAllWorkByTag_cancelsDependentWork() assertThat(workSpecDao.getState(work2.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work3.getStringId()), is(not(CANCELLED))); assertThat(workSpecDao.getState(work4.getStringId()), is(CANCELLED)); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(4)).onCancelled(workSnapshotCaptor.capture(), null); - List cancelledInfos = workSnapshotCaptor.getAllValues(); - Matcher isOneOfCancelled = isOneOf( - work0.getId(), work1.getId(), work2.getId(), work4.getId()); - assertThat(cancelledInfos.get(0).getId(), isOneOfCancelled); - assertThat(cancelledInfos.get(0).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(1).getId(), isOneOfCancelled); - assertThat(cancelledInfos.get(1).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(2).getId(), isOneOfCancelled); - assertThat(cancelledInfos.get(2).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(3).getId(), isOneOfCancelled); - assertThat(cancelledInfos.get(3).getState(), is(CANCELLED)); } @Test @@ -2045,14 +1867,6 @@ public void testCancelWorkByName() throws ExecutionException, InterruptedExcepti WorkSpecDao workSpecDao = mDatabase.workSpecDao(); assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED)); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(2)).onCancelled(workSnapshotCaptor.capture(), null); - List cancelledInfos = workSnapshotCaptor.getAllValues(); - assertThat(cancelledInfos.get(0).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(0).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(1).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(1).getState(), is(CANCELLED)); } @Test @@ -2073,12 +1887,6 @@ public void testCancelWorkByName_ignoresFinishedWork() WorkSpecDao workSpecDao = mDatabase.workSpecDao(); assertThat(workSpecDao.getState(work0.getStringId()), is(SUCCEEDED)); assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED)); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(1)).onCancelled(workSnapshotCaptor.capture(), null); - List cancelledInfos = workSnapshotCaptor.getAllValues(); - assertThat(cancelledInfos.get(0).getId(), is(work1.getId())); - assertThat(cancelledInfos.get(0).getState(), is(CANCELLED)); } @Test @@ -2102,14 +1910,6 @@ public void testCancelAllWork() throws ExecutionException, InterruptedException assertThat(workSpecDao.getState(work0.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work1.getStringId()), is(CANCELLED)); assertThat(workSpecDao.getState(work2.getStringId()), is(SUCCEEDED)); - - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mSchedulingEventListener, times(2)).onCancelled(workSnapshotCaptor.capture(), null); - List cancelledInfos = workSnapshotCaptor.getAllValues(); - assertThat(cancelledInfos.get(0).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(0).getState(), is(CANCELLED)); - assertThat(cancelledInfos.get(1).getId(), isOneOf(work0.getId(), work1.getId())); - assertThat(cancelledInfos.get(1).getState(), is(CANCELLED)); } @Test diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java index 9c031b4da733e..b1c3a8f946175 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java +++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java @@ -75,7 +75,6 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.PeriodicWorkRequest; import androidx.work.ProgressUpdater; -import androidx.work.ScheduleEventListener; import androidx.work.Tracer; import androidx.work.WorkInfo; import androidx.work.WorkRequest; @@ -140,14 +139,12 @@ public class WorkerWrapperTest extends DatabaseTest { private TestWorkerExceptionHandler mWorkerExceptionHandler; private Tracer mTracer; private ExecutionEventListener mWorkExecutionListener; - private ScheduleEventListener mWorkSchedulingListener; @Before public void setUp() { mContext = ApplicationProvider.getApplicationContext(); mTracer = mock(Tracer.class); mWorkExecutionListener = mock(ExecutionEventListener.class); - mWorkSchedulingListener = mock(ScheduleEventListener.class); // Turn on tracing so we can ensure trace sections are correctly emitted. when(mTracer.isEnabled()).thenReturn(true); mWorkerExceptionHandler = new TestWorkerExceptionHandler(); @@ -158,7 +155,6 @@ public void setUp() { .setWorkerExecutionExceptionHandler(mWorkerExceptionHandler) .setTracer(mTracer) .setExecutionEventListener(mWorkExecutionListener) - .setScheduleEventListener(mWorkSchedulingListener) .build(); mWorkTaskExecutor = new WorkManagerTaskExecutor(mConfiguration.getTaskExecutor()); mWorkSpecDao = mDatabase.workSpecDao(); @@ -580,12 +576,6 @@ public void testDependencies_enqueuesBlockedDependentsOnSuccess() isOneOf(ENQUEUED, RUNNING, SUCCEEDED)); assertThat(mWorkSpecDao.getState(cancelledWork.getStringId()), is(CANCELLED)); assertBeginEndTraceSpans(prerequisiteWork.getWorkSpec()); - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mWorkSchedulingListener, times(1)).onUnblocked(workSnapshotCaptor.capture(), - null); - WorkInfo unblockSnapshot = workSnapshotCaptor.getValue(); - assertThat(unblockSnapshot.getId(), is(work.getId())); - assertThat(unblockSnapshot.getState(), is(ENQUEUED)); } @Test @@ -624,12 +614,6 @@ public void testDependencies_failsUncancelledDependentsOnFailure() assertThat(mWorkSpecDao.getState(prerequisiteWork.getStringId()), is(FAILED)); assertThat(mWorkSpecDao.getState(work.getStringId()), is(FAILED)); assertThat(mWorkSpecDao.getState(cancelledWork.getStringId()), is(CANCELLED)); - ArgumentCaptor workSnapshotCaptor = ArgumentCaptor.forClass(WorkInfo.class); - verify(mWorkSchedulingListener, times(1)).onPrerequisiteFailed(workSnapshotCaptor.capture(), - null); - WorkInfo failSnapshot = workSnapshotCaptor.getValue(); - assertThat(failSnapshot.getId(), is(work.getId())); - assertThat(failSnapshot.getState(), is(FAILED)); } @Test diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java index 5b4809e79bf4b..1b44750d4556f 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java +++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java @@ -72,7 +72,6 @@ import org.junit.runner.RunWith; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @RunWith(AndroidJUnit4.class) @@ -182,11 +181,7 @@ public void testOnStopJob_DoesNotRescheduleWhenCancelled() { mInstrumentation.runOnMainSync(() -> { JobParameters mockParams = createMockJobParameters(work.getStringId()); assertThat(mSystemJobServiceSpy.onStartJob(mockParams), is(true)); - try { - mWorkManagerImpl.cancelWorkById(work.getId()).getResult().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } + mWorkManagerImpl.cancelWorkById(work.getId()); assertThat(mSystemJobServiceSpy.onStopJob(mockParams), is(false)); }); } diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/EnqueueRunnableTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/EnqueueRunnableTest.kt index 594b857ee996f..46a01ab307aa5 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/EnqueueRunnableTest.kt +++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/EnqueueRunnableTest.kt @@ -31,7 +31,6 @@ import androidx.work.testutils.TestEnv import androidx.work.testutils.WorkManager import androidx.work.worker.TestWorker import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith @@ -59,7 +58,7 @@ class EnqueueRunnableTest { ) @Test - fun testCheckScheduling() = runBlocking { + fun testCheckScheduling() { val request1 = OneTimeWorkRequest.Builder(TestWorker::class.java).build() val impl1 = WorkContinuationImpl(wm, "name", ExistingWorkPolicy.KEEP, listOf(request1)) EnqueueRunnable.enqueue(impl1) diff --git a/work/work-runtime/src/main/java/androidx/work/Configuration.kt b/work/work-runtime/src/main/java/androidx/work/Configuration.kt index 97590ae2b1dca..bb04bb302502a 100644 --- a/work/work-runtime/src/main/java/androidx/work/Configuration.kt +++ b/work/work-runtime/src/main/java/androidx/work/Configuration.kt @@ -172,14 +172,6 @@ public class Configuration internal constructor(builder: Builder) { return executionEventListener } - @property:ExperimentalEventsApi private val scheduleEventListener: ScheduleEventListener? - - /** The [ScheduleEventListener] that listens to work execution events for all workers. */ - @ExperimentalEventsApi - public fun getScheduleEventListener(): ScheduleEventListener? { - return scheduleEventListener - } - /** * @return The [Tracer] instance that can be used by [WorkManager] to record trace spans when * executing [WorkRequest]s. @@ -243,7 +235,6 @@ public class Configuration internal constructor(builder: Builder) { contentUriTriggerWorkersLimit = builder.contentUriTriggerWorkersLimit isMarkingJobsAsImportantWhileForeground = builder.markJobsAsImportantWhileForeground executionEventListener = builder.executionEventListener - scheduleEventListener = builder.scheduleEventListener tracer = builder.tracer ?: createDefaultTracer() enableRepresentativeJobs = builder.enableRepresentativeJobs } @@ -270,7 +261,6 @@ public class Configuration internal constructor(builder: Builder) { internal var contentUriTriggerWorkersLimit: Int = DEFAULT_CONTENT_URI_TRIGGERS_WORKERS_LIMIT internal var markJobsAsImportantWhileForeground: Boolean = true internal var executionEventListener: ExecutionEventListener? = null - internal var scheduleEventListener: ScheduleEventListener? = null internal var tracer: Tracer? = null internal var enableRepresentativeJobs: Boolean = false @@ -307,7 +297,6 @@ public class Configuration internal constructor(builder: Builder) { markJobsAsImportantWhileForeground = configuration.isMarkingJobsAsImportantWhileForeground executionEventListener = configuration.executionEventListener - scheduleEventListener = configuration.scheduleEventListener tracer = configuration.tracer } @@ -623,22 +612,6 @@ public class Configuration internal constructor(builder: Builder) { return this } - /** - * Set a [ScheduleEventListener] to run whenever work scheduling events occur for any - * worker. - * - * These callbacks will be invoked on a thread bound to [Configuration.taskExecutor]. - * - * @param listener [ScheduleEventListener] to set - * @return This [Builder] instance - */ - @SuppressLint("ExecutorRegistration") // Developer can configure taskExecutor directly - @ExperimentalEventsApi - public fun setScheduleEventListener(listener: ScheduleEventListener): Builder { - this.scheduleEventListener = listener - return this - } - /** * Specifies the [Tracer] that can be used by [WorkManager] to record trace spans. * diff --git a/work/work-runtime/src/main/java/androidx/work/Operation.kt b/work/work-runtime/src/main/java/androidx/work/Operation.kt index 5683eed93da6e..113b0ad998d0a 100644 --- a/work/work-runtime/src/main/java/androidx/work/Operation.kt +++ b/work/work-runtime/src/main/java/androidx/work/Operation.kt @@ -19,12 +19,12 @@ package androidx.work +import androidx.concurrent.futures.CallbackToFutureAdapter import androidx.concurrent.futures.await import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.common.util.concurrent.ListenableFuture import java.util.concurrent.Executor -import kotlinx.coroutines.asCoroutineDispatcher /** * Awaits an [Operation] without blocking a thread. @@ -39,19 +39,21 @@ internal fun launchOperation( tracer: Tracer, label: String, executor: Executor, - block: suspend () -> Unit, + block: () -> Unit, ): Operation { val liveData = MutableLiveData(Operation.IN_PROGRESS) val future = - launchFuture(executor.asCoroutineDispatcher()) { - tracer.traced(label) { - try { - block() - liveData.postValue(Operation.SUCCESS) - Operation.SUCCESS - } catch (t: Throwable) { - liveData.postValue(Operation.State.FAILURE(t)) - throw t + CallbackToFutureAdapter.getFuture { completer -> + executor.execute { + tracer.traced(label) { + try { + block() + liveData.postValue(Operation.SUCCESS) + completer.set(Operation.SUCCESS) + } catch (t: Throwable) { + liveData.postValue(Operation.State.FAILURE(t)) + completer.setException(t) + } } } } diff --git a/work/work-runtime/src/main/java/androidx/work/ScheduleEventListener.kt b/work/work-runtime/src/main/java/androidx/work/ScheduleEventListener.kt deleted file mode 100644 index 0d1568842c4b0..0000000000000 --- a/work/work-runtime/src/main/java/androidx/work/ScheduleEventListener.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.work - -/** Listener interface that is called for events related to a worker's scheduling. */ -@ExperimentalConfigurationApi -public interface ScheduleEventListener { - - /** - * Called when a work request is enqueued from the app e.g. [WorkManager.enqueue]. - * - * @param workInfo Snapshot of the work info - */ - public suspend fun onEnqueued(workInfo: WorkInfo) {} - - /** - * Called when a work request is updated from the app e.g. [WorkManager.updateWork]. - * - * @param oldWorkInfo Snapshot of the work info before the update - * @param updatedWorkInfo Snapshot of the work info after the update - */ - public suspend fun onUpdated(oldWorkInfo: WorkInfo, updatedWorkInfo: WorkInfo) {} - - /** - * Called when a work request is no longer waiting on any of its prerequisite work. If the work - * has no prerequisite work, this is called immediately after [onEnqueued]. - * - * @param workInfo Snapshot of the work info - */ - public suspend fun onUnblocked(workInfo: WorkInfo) {} - - /** - * Called when a work request is canceled from the app e.g. [WorkManager.cancelWorkById]. - * - * @param workInfo Snapshot of the work info - */ - public suspend fun onCancelled(workInfo: WorkInfo) {} - - /** - * Called when a work request fails because a prerequisite work fails. - * - * @param workInfo Snapshot of the work info - */ - public suspend fun onPrerequisiteFailed(workInfo: WorkInfo) {} -} diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java index ced5c280319bb..22e7f4f889868 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java +++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkContinuationImpl.java @@ -16,7 +16,7 @@ package androidx.work.impl; -import static androidx.work.impl.utils.EnqueueRunnableKt.launchEnqueue; +import static androidx.work.OperationKt.launchOperation; import android.text.TextUtils; @@ -30,11 +30,14 @@ import androidx.work.WorkContinuation; import androidx.work.WorkInfo; import androidx.work.WorkRequest; +import androidx.work.impl.utils.EnqueueRunnable; import androidx.work.impl.utils.StatusRunnable; import androidx.work.impl.workers.CombineContinuationsWorker; import com.google.common.util.concurrent.ListenableFuture; +import kotlin.Unit; + import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -183,11 +186,14 @@ public WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl, if (!mEnqueued) { // The runnable walks the hierarchy of the continuations // and marks them enqueued using the markEnqueued() method, parent first. - mOperation = launchEnqueue( + mOperation = launchOperation( mWorkManagerImpl.getConfiguration().getTracer(), "EnqueueRunnable_" + getExistingWorkPolicy().name(), mWorkManagerImpl.getWorkTaskExecutor().getSerialTaskExecutor(), - this); + () -> { + EnqueueRunnable.enqueue(this); + return Unit.INSTANCE; + }); } else { Logger.get().warning(TAG, "Already enqueued work ids (" + TextUtils.join(", ", mIds) + ")"); diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt index 9b6ff6ae878bd..cb38911ebd186 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt @@ -18,7 +18,6 @@ package androidx.work.impl import androidx.annotation.RestrictTo -import androidx.room.withTransaction import androidx.work.Configuration import androidx.work.ExistingWorkPolicy import androidx.work.Operation @@ -26,15 +25,14 @@ import androidx.work.WorkInfo import androidx.work.WorkManager.UpdateResult import androidx.work.WorkManager.UpdateResult.APPLIED_FOR_NEXT_RUN import androidx.work.WorkRequest +import androidx.work.executeAsync import androidx.work.impl.model.WorkSpec -import androidx.work.impl.model.getWorkInfo import androidx.work.impl.utils.EnqueueRunnable import androidx.work.impl.utils.wrapWorkSpecIfNeeded -import androidx.work.launchFuture import androidx.work.launchOperation import com.google.common.util.concurrent.ListenableFuture -private suspend fun updateWorkImpl( +private fun updateWorkImpl( processor: Processor, workDatabase: WorkDatabase, configuration: Configuration, @@ -56,12 +54,9 @@ private suspend fun updateWorkImpl( } val isEnqueued = processor.isEnqueued(workSpecId) if (!isEnqueued) schedulers.forEach { scheduler -> scheduler.cancel(workSpecId) } - val scheduleListener = configuration.getScheduleEventListener() - workDatabase.withTransaction { + workDatabase.runInTransaction { val workSpecDao = workDatabase.workSpecDao() val workTagDao = workDatabase.workTagDao() - val oldWorkInfo = - if (scheduleListener != null) workSpecDao.getWorkInfo(workSpecId) else null // should keep state BLOCKING, preserving the chain, or possibly RUNNING // preserving run attempt count, to calculate back off correctly, and enqueue/override time @@ -93,7 +88,6 @@ private suspend fun updateWorkImpl( workSpecDao.markWorkSpecScheduled(workSpecId, WorkSpec.SCHEDULE_NOT_REQUESTED_YET) workDatabase.workProgressDao().delete(workSpecId) } - scheduleListener?.onUpdated(oldWorkInfo!!, workSpecDao.getWorkInfo(workSpecId)!!) } if (!isEnqueued) Schedulers.schedule(configuration, workDatabase, schedulers) return if (isEnqueued) APPLIED_FOR_NEXT_RUN else UpdateResult.APPLIED_IMMEDIATELY @@ -102,7 +96,7 @@ private suspend fun updateWorkImpl( internal fun WorkManagerImpl.updateWorkImpl( workRequest: WorkRequest ): ListenableFuture { - return launchFuture(workTaskExecutor.taskCoroutineDispatcher) { + return workTaskExecutor.serialTaskExecutor.executeAsync("updateWorkImpl") { updateWorkImpl( processor, workDatabase, @@ -125,15 +119,9 @@ public fun WorkManagerImpl.enqueueUniquelyNamedPeriodic( "enqueueUniquePeriodic_$name", workTaskExecutor.serialTaskExecutor, ) { - val enqueueNew = suspend { + val enqueueNew = { val requests = listOf(workRequest) - val continuation = - WorkContinuationImpl( - this@enqueueUniquelyNamedPeriodic, - name, - ExistingWorkPolicy.KEEP, - requests, - ) + val continuation = WorkContinuationImpl(this, name, ExistingWorkPolicy.KEEP, requests) EnqueueRunnable.enqueue(continuation) } diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt index 8d8fd90a5d827..88b8de7830c98 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt @@ -37,12 +37,9 @@ import androidx.work.impl.model.WorkGenerationalId import androidx.work.impl.model.WorkSpec import androidx.work.impl.model.WorkSpecDao import androidx.work.impl.model.generationalId -import androidx.work.impl.model.getAllDependentWork import androidx.work.impl.model.getWorkInfo -import androidx.work.impl.model.getWorkInfos import androidx.work.impl.utils.WorkForegroundUpdater import androidx.work.impl.utils.WorkProgressUpdater -import androidx.work.impl.utils.dispatchScheduleEvents import androidx.work.impl.utils.safeAccept import androidx.work.impl.utils.taskexecutor.TaskExecutor import androidx.work.impl.utils.workForeground @@ -56,6 +53,7 @@ import java.util.concurrent.Callable import java.util.concurrent.CancellationException import java.util.concurrent.ExecutionException import java.util.concurrent.Future +import kotlin.collections.removeLast as removeLastKt import kotlin.coroutines.coroutineContext import kotlin.coroutines.resumeWithException import kotlinx.coroutines.CancellableContinuation @@ -90,7 +88,6 @@ public class WorkerWrapper internal constructor(builder: Builder) { private val workerJob = Job() private var startedWork = false - private var modifiedDependents = mutableListOf() public val workGenerationalId: WorkGenerationalId get() = workSpec.generationalId() @@ -144,9 +141,6 @@ public class WorkerWrapper internal constructor(builder: Builder) { if (resolution.recoverable) resetWorkerStatus(STOP_REASON_NOT_STOPPED) else onWorkFailed(Failure()) } - configuration - .getScheduleEventListener() - ?.dispatchScheduleEvents(workSpecDao.getWorkInfos(modifiedDependents)) } needsReschedule } @@ -486,16 +480,14 @@ public class WorkerWrapper internal constructor(builder: Builder) { } private fun iterativelyFailWorkAndDependents(workSpecId: String) { - val idsToFail = mutableListOf(workSpecId) - idsToFail.addAll(dependencyDao.getAllDependentWork(workSpecId)) - for (id in idsToFail) { + val idsToProcess = mutableListOf(workSpecId) + while (idsToProcess.isNotEmpty()) { + val id = idsToProcess.removeLastKt() // Don't fail already cancelled work. if (workSpecDao.getState(id) !== WorkInfo.State.CANCELLED) { workSpecDao.setState(WorkInfo.State.FAILED, id) - if (id != workSpecId) { - modifiedDependents.add(id) - } } + idsToProcess.addAll(dependencyDao.getDependentWorkIds(id)) } } @@ -546,7 +538,6 @@ public class WorkerWrapper internal constructor(builder: Builder) { logi(TAG) { "Setting status to enqueued for $dependentWorkId" } workSpecDao.setState(WorkInfo.State.ENQUEUED, dependentWorkId) workSpecDao.setLastEnqueueTime(dependentWorkId, currentTimeMillis) - modifiedDependents.add(dependentWorkId) } } return false diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt index 2004d9f62d0e8..daf03287acc3a 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.kt @@ -152,13 +152,6 @@ internal class NetworkStateTracker24(context: Context, taskExecutor: TaskExecuto @Volatile private var isBlocked: Boolean = false override fun readSystemState(): NetworkState { - if (Build.VERSION.SDK_INT >= 28) { - val capabilities = - connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - if (capabilities != null) { - return getActiveNetworkState(capabilities, isBlocked) - } - } return getActiveNetworkState(connectivityManager, isBlocked) } diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/DependencyDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/DependencyDao.kt index 38c39e07fb09b..1ae33cad45986 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/model/DependencyDao.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/model/DependencyDao.kt @@ -19,7 +19,6 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import kotlin.collections.removeLast as removeLastKt /** The Data Access Object for [Dependency]. */ @Dao @@ -73,15 +72,3 @@ public interface DependencyDao { @Query("SELECT COUNT(*)>0 FROM dependency WHERE prerequisite_id=:id") public fun hasDependents(id: String): Boolean } - -/** Get all dependent work, including dependents of dependents. */ -public fun DependencyDao.getAllDependentWork(id: String): List { - val dependentWork = mutableListOf() - val idsToProcess = getDependentWorkIds(id).toMutableList() - while (idsToProcess.isNotEmpty()) { - val id = idsToProcess.removeLastKt() - dependentWork.add(id) - idsToProcess.addAll(getDependentWorkIds(id)) - } - return dependentWork -} diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt index 8cf45a11dc397..06b7577279502 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt @@ -522,9 +522,6 @@ public fun WorkSpecDao.getWorkStatusPojoFlowForTag( public fun WorkSpecDao.getWorkInfo(id: String): WorkInfo? = getWorkStatusPojoForId(id)?.toWorkInfo() -internal fun WorkSpecDao.getWorkInfos(ids: List): List = - getWorkStatusPojoForIds(ids).map { it.toWorkInfo() } - internal fun Flow>.dedup( dispatcher: CoroutineDispatcher ): Flow> = diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt index 80c2f76abe2bb..0704ce12abdbd 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt @@ -18,38 +18,22 @@ package androidx.work.impl.utils import android.app.job.JobParameters -import androidx.room.withTransaction import androidx.work.Operation +import androidx.work.WorkInfo import androidx.work.impl.Schedulers import androidx.work.impl.WorkDatabase import androidx.work.impl.WorkManagerImpl -import androidx.work.impl.model.getAllDependentWork -import androidx.work.impl.model.getWorkInfos import androidx.work.launchOperation import java.util.UUID +import kotlin.collections.removeLast as removeLastKt -/** Cancel work and its dependents and dispatch schedule events */ -private suspend fun cancel(workManagerImpl: WorkManagerImpl, workSpecId: String) { - val toCancel = workManagerImpl.workDatabase.getWorkToCancel(workSpecId) - val workSpecDao = workManagerImpl.workDatabase.workSpecDao() - for (id in toCancel) { - workSpecDao.setCancelledState(id) - } +private fun cancel(workManagerImpl: WorkManagerImpl, workSpecId: String) { + iterativelyCancelWorkAndDependents(workManagerImpl.workDatabase, workSpecId) val processor = workManagerImpl.processor processor.stopAndCancelWork(workSpecId, JobParameters.STOP_REASON_CANCELLED_BY_APP) for (scheduler in workManagerImpl.schedulers) { scheduler.cancel(workSpecId) } - val scheduleEventListener = workManagerImpl.configuration.getScheduleEventListener() - scheduleEventListener?.dispatchScheduleEvents(workSpecDao.getWorkInfos(toCancel)) -} - -private fun WorkDatabase.getWorkToCancel(workSpecId: String): List { - val workSpecDao = workSpecDao() - val dependencyDao = dependencyDao() - val toCancel = mutableListOf(workSpecId) - toCancel.addAll(dependencyDao.getAllDependentWork(workSpecId)) - return toCancel.filter { id -> workSpecDao.getState(id)?.let { !it.isFinished } ?: false } } private fun reschedulePendingWorkers(workManagerImpl: WorkManagerImpl) { @@ -60,6 +44,21 @@ private fun reschedulePendingWorkers(workManagerImpl: WorkManagerImpl) { ) } +private fun iterativelyCancelWorkAndDependents(workDatabase: WorkDatabase, workSpecId: String) { + val workSpecDao = workDatabase.workSpecDao() + val dependencyDao = workDatabase.dependencyDao() + val idsToProcess = mutableListOf(workSpecId) + while (idsToProcess.isNotEmpty()) { + val id = idsToProcess.removeLastKt() + // Don't fail already cancelled work. + val state = workSpecDao.getState(id) + if (state !== WorkInfo.State.SUCCEEDED && state !== WorkInfo.State.FAILED) { + workSpecDao.setCancelledState(id) + } + idsToProcess.addAll(dependencyDao.getDependentWorkIds(id)) + } +} + /** * Cancels work for a specific id. * @@ -74,7 +73,7 @@ public fun forId(id: UUID, workManagerImpl: WorkManagerImpl): Operation = workManagerImpl.workTaskExecutor.serialTaskExecutor, ) { val workDatabase = workManagerImpl.workDatabase - workDatabase.withTransaction { cancel(workManagerImpl, id.toString()) } + workDatabase.runInTransaction { cancel(workManagerImpl, id.toString()) } reschedulePendingWorkers(workManagerImpl) } @@ -92,7 +91,7 @@ public fun forTag(tag: String, workManagerImpl: WorkManagerImpl): Operation = executor = workManagerImpl.workTaskExecutor.serialTaskExecutor, ) { val workDatabase = workManagerImpl.workDatabase - workDatabase.withTransaction { + workDatabase.runInTransaction { val workSpecDao = workDatabase.workSpecDao() val workSpecIds = workSpecDao.getUnfinishedWorkWithTag(tag) for (workSpecId in workSpecIds) { @@ -119,10 +118,9 @@ public fun forName(name: String, workManagerImpl: WorkManagerImpl): Operation = reschedulePendingWorkers(workManagerImpl) } -/** Cancels work labelled with a specific name without rescheduling pending */ -public suspend fun forNameInline(name: String, workManagerImpl: WorkManagerImpl) { +public fun forNameInline(name: String, workManagerImpl: WorkManagerImpl) { val workDatabase = workManagerImpl.workDatabase - workDatabase.withTransaction { + workDatabase.runInTransaction { val workSpecDao = workDatabase.workSpecDao() val workSpecIds = workSpecDao.getUnfinishedWorkWithName(name) for (workSpecId in workSpecIds) { @@ -144,7 +142,7 @@ public fun forAll(workManagerImpl: WorkManagerImpl): Operation = workManagerImpl.workTaskExecutor.serialTaskExecutor, ) { val workDatabase = workManagerImpl.workDatabase - workDatabase.withTransaction { + workDatabase.runInTransaction { val workSpecDao = workDatabase.workSpecDao() val workSpecIds = workSpecDao.getAllUnfinishedWork() for (workSpecId in workSpecIds) { diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.kt index 95a9b77acd893..bc942f45fe23e 100644 --- a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.kt +++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.kt @@ -17,11 +17,9 @@ package androidx.work.impl.utils import android.text.TextUtils import androidx.annotation.RestrictTo -import androidx.room.withTransaction +import androidx.annotation.VisibleForTesting import androidx.work.ExistingWorkPolicy import androidx.work.Logger -import androidx.work.Operation -import androidx.work.Tracer import androidx.work.WorkInfo import androidx.work.WorkRequest import androidx.work.impl.Schedulers @@ -30,17 +28,15 @@ import androidx.work.impl.WorkManagerImpl import androidx.work.impl.model.Dependency import androidx.work.impl.model.WorkName import androidx.work.impl.model.WorkSpec -import androidx.work.impl.model.getWorkInfos -import androidx.work.launchOperation -import java.util.concurrent.Executor /** Manages the enqueuing of a [WorkContinuationImpl]. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public object EnqueueRunnable { private val TAG = Logger.tagWithPrefix("EnqueueRunnable") - /** Enqueues the given work continuation and schedules if necessary. */ - public suspend fun enqueue(workContinuation: WorkContinuationImpl) { + /** Enqueues the given workContinuation. */ + @JvmStatic + public fun enqueue(workContinuation: WorkContinuationImpl) { check(!workContinuation.hasCycles()) { "WorkContinuation has cycles ($workContinuation)" } val needsScheduling = addToDatabase(workContinuation) if (needsScheduling) { @@ -49,26 +45,32 @@ public object EnqueueRunnable { } /** - * Adds the [WorkSpec]'s to the datastore, parent first. - * - * @return the list of all work that was successfully persisted to the database + * Adds the [WorkSpec]'s to the datastore, parent first. Schedules work on the background + * scheduler, if transaction is successful. */ + @VisibleForTesting @Suppress("deprecation") - private suspend fun addToDatabase(workContinuation: WorkContinuationImpl): Boolean { + public fun addToDatabase(workContinuation: WorkContinuationImpl): Boolean { val workManagerImpl = workContinuation.workManagerImpl val workDatabase = workManagerImpl.workDatabase - return workDatabase.withTransaction { + workDatabase.beginTransaction() + try { checkContentUriTriggerWorkerLimits( workDatabase, workManagerImpl.configuration, workContinuation, ) - processContinuation(workContinuation) + val needsScheduling = processContinuation(workContinuation) + workDatabase.setTransactionSuccessful() + return needsScheduling + } finally { + workDatabase.endTransaction() } } /** Schedules work on the background scheduler. */ - private fun scheduleWorkInBackground(workContinuation: WorkContinuationImpl) { + @VisibleForTesting + public fun scheduleWorkInBackground(workContinuation: WorkContinuationImpl) { val workManager = workContinuation.workManagerImpl Schedulers.schedule( workManager.configuration, @@ -77,7 +79,7 @@ public object EnqueueRunnable { ) } - private suspend fun processContinuation(workContinuation: WorkContinuationImpl): Boolean { + private fun processContinuation(workContinuation: WorkContinuationImpl): Boolean { var needsScheduling = false val parents = workContinuation.parents if (parents != null) { @@ -99,7 +101,7 @@ public object EnqueueRunnable { return needsScheduling } - private suspend fun enqueueContinuation(workContinuation: WorkContinuationImpl): Boolean { + private fun enqueueContinuation(workContinuation: WorkContinuationImpl): Boolean { val prerequisiteIds = WorkContinuationImpl.prerequisitesFor(workContinuation) val needsScheduling = @@ -120,14 +122,13 @@ public object EnqueueRunnable { * * @return `true` If there is any scheduling to be done. */ - private suspend fun enqueueWorkWithPrerequisites( + private fun enqueueWorkWithPrerequisites( workManagerImpl: WorkManagerImpl, workList: List, prerequisiteIds: Array, name: String?, existingWorkPolicy: ExistingWorkPolicy, ): Boolean { - val scheduleListener = workManagerImpl.configuration.getScheduleEventListener() var prerequisiteIds = prerequisiteIds var needsScheduling = false @@ -198,14 +199,6 @@ public object EnqueueRunnable { val workSpecDao = workDatabase.workSpecDao() val idAndStates: List = workSpecDao.getWorkSpecIdAndStatesForName(name) - val ids = idAndStates.map { (id, _) -> id } - // Modify the snapshot to have the cancelled state since we're avoiding - // the unnecessary database cancel. - scheduleListener?.dispatchScheduleEvents( - workSpecDao.getWorkStatusPojoForIds(ids).map { - it.copy(state = WorkInfo.State.CANCELLED).toWorkInfo() - } - ) for (idAndState in idAndStates) { workSpecDao.delete(idAndState.id) } @@ -290,24 +283,6 @@ public object EnqueueRunnable { workDatabase.workNameDao().insert(WorkName(name!!, work.stringId)) } } - scheduleListener?.dispatchScheduleEvents( - workDatabase.workSpecDao().getWorkInfos(workList.map { it.stringId }), - isEnqueue = true, - ) return needsScheduling } } - -internal fun launchEnqueue( - tracer: Tracer, - label: String, - executor: Executor, - continuation: WorkContinuationImpl, -): Operation { - return launchOperation( - tracer, - label, - executor, - suspend { EnqueueRunnable.enqueue(continuation) }, - ) -} diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/ScheduleEventDispatch.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/ScheduleEventDispatch.kt deleted file mode 100644 index f94fd048c28da..0000000000000 --- a/work/work-runtime/src/main/java/androidx/work/impl/utils/ScheduleEventDispatch.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.work.impl.utils - -import androidx.work.Operation -import androidx.work.ScheduleEventListener -import androidx.work.WorkInfo -import androidx.work.WorkInfo.State.CANCELLED -import androidx.work.WorkInfo.State.ENQUEUED -import androidx.work.WorkInfo.State.FAILED - -internal suspend fun ScheduleEventListener.dispatchScheduleEvents(work: List) { - dispatchScheduleEvents(work, false) -} - -/** - * Dispatch the appropriate schedule event for a list of work infos that had their [WorkInfo.State] - * modified due to an [Operation] or worker finishing. e.g. a cancel would cancel its dependents, a - * worker finishing may unblock its dependents. - * - * This should not include work that was not modified. For example a worker failing will not modify - * the state of a dependents that are [CANCELLED] and so we shouldn't send a - * [ScheduleEventListener.onPrerequisiteFailed] event. - * - * @param work work infos that had its work state modified. - * @param isEnqueue true if this is after the works are enqueued - */ -internal suspend fun ScheduleEventListener.dispatchScheduleEvents( - work: List, - isEnqueue: Boolean, -) { - if (work.isEmpty()) { - return - } - for (work in work) { - if (isEnqueue) { - onEnqueued(work) - } - when (work.state) { - FAILED -> onPrerequisiteFailed(work) - CANCELLED -> onCancelled(work) - ENQUEUED -> onUnblocked(work) - else -> {} - } - } -} diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt b/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt index 287300d1551f2..59346c45ac429 100644 --- a/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt +++ b/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerRealExecutorTest.kt @@ -30,7 +30,6 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkInfo.State.ENQUEUED -import androidx.work.await import androidx.work.impl.WorkManagerImpl import androidx.work.impl.model.WorkSpec import androidx.work.impl.utils.taskexecutor.SerialExecutor @@ -63,7 +62,7 @@ class TestSchedulerRealExecutorTest { fun testWorker_withDependentWork_shouldSucceedSynchronously() { val request = OneTimeWorkRequest.from(TestWorker::class.java) val dependentRequest = OneTimeWorkRequest.from(TestWorker::class.java) - wm.beginWith(request).then(dependentRequest).enqueue().result.get() + wm.beginWith(request).then(dependentRequest).enqueue() awaitSuccess(dependentRequest.id) } diff --git a/xr/arcore/arcore-openxr/proguard-rules.pro b/xr/arcore/arcore-openxr/proguard-rules.pro index ebcf0b0a4da84..14574198850c3 100644 --- a/xr/arcore/arcore-openxr/proguard-rules.pro +++ b/xr/arcore/arcore-openxr/proguard-rules.pro @@ -1,5 +1,33 @@ -# Prevent the OpenXR classes from being obfuscated as they are created from native code. --keep class androidx.xr.arcore.openxr.** { *; } --keep class androidx.xr.arcore.openxr.**$* { *; } --keep class * extends androidx.xr.arcore.openxr.** { *; } --keep class * extends androidx.xr.arcore.openxr.**$* { *; } +# androidx.xr.arcore.openxr.AnchorState is referenced by native code. +-keep class androidx.xr.arcore.openxr.AnchorState { *; } +-keep class androidx.xr.arcore.openxr.AnchorStateKt { *; } +# androidx.xr.arcore.openxr.AugmentedObjectState is referenced by native code. +-keep class androidx.xr.arcore.openxr.AugmentedObjectState { *; } +# androidx.xr.arcore.openxr.EyeData is referenced by native code. +-keep class androidx.xr.arcore.openxr.EyeData { *; } +-keep class androidx.xr.arcore.openxr.EyeDataKt { *; } +# androidx.xr.arcore.openxr.EyeStatus is referenced by native code. +-keep class androidx.xr.arcore.openxr.EyeStatus { *; } +-keep class androidx.xr.arcore.openxr.EyeStatus$Companion { *; } +# androidx.xr.arcore.openxr.EyeTrackingState is referenced by native code. +-keep class androidx.xr.arcore.openxr.EyeTrackingState { *; } +-keep class androidx.xr.arcore.openxr.EyeTrackingState$Companion { *; } +# androidx.xr.arcore.openxr.EyesInfo is referenced by native code. +-keep class androidx.xr.arcore.openxr.EyesInfo { *; } +-keep class androidx.xr.arcore.openxr.EyesInfoKt { *; } +# androidx.xr.arcore.openxr.FaceState is referenced by native code. +-keep class androidx.xr.arcore.openxr.FaceState { *; } +-keep class androidx.xr.arcore.openxr.FaceStateKt { *; } +# androidx.xr.arcore.openxr.HitData is referenced by native code. +-keep class androidx.xr.arcore.openxr.HitData { *; } +# androidx.xr.arcore.openxr.OpenXrAnchor is referenced by native code. +-keep class androidx.xr.arcore.openxr.OpenXrAnchorKt { *; } +# androidx.xr.arcore.openxr.OpenXrPlane is referenced by native code. +-keep class androidx.xr.arcore.openxr.OpenXrPlaneKt { *; } +# androidx.xr.arcore.openxr.OpenXrRuntime is referenced by native code. +-keep class androidx.xr.arcore.openxr.OpenXrRuntime { *; } +# androidx.xr.arcore.openxr.PlaneState is referenced by native code. +-keep class androidx.xr.arcore.openxr.PlaneState { *; } +-keep class androidx.xr.arcore.openxr.PlaneStateKt { *; } +# androidx.xr.arcore.openxr.ViewCameraState is referenced by native code. +-keep class androidx.xr.arcore.openxr.ViewCameraState { *; } diff --git a/xr/arcore/arcore-runtime/proguard-rules.pro b/xr/arcore/arcore-runtime/proguard-rules.pro index 2c34cc762cd2b..4da3c83038e45 100644 --- a/xr/arcore/arcore-runtime/proguard-rules.pro +++ b/xr/arcore/arcore-runtime/proguard-rules.pro @@ -1,4 +1,11 @@ --keep class androidx.xr.arcore.runtime.** { *; } --keep class androidx.xr.arcore.runtime.**$* { *; } --keep class * extends androidx.xr.arcore.runtime.** { *; } --keep class * extends androidx.xr.arcore.runtime.**$* { *; } \ No newline at end of file +# androidx.xr.arcore.runtime.Anchor is referenced by native code. +-keep class androidx.xr.arcore.runtime.Anchor { *; } +-keep class androidx.xr.arcore.runtime.Anchor$PersistenceState { *; } +-keep class androidx.xr.arcore.runtime.Anchor$PersistenceState$Companion { *; } +# androidx.xr.arcore.runtime.Geospatial is referenced by native code. +-keep class androidx.xr.arcore.runtime.Geospatial { *; } +-keep class androidx.xr.arcore.runtime.Geospatial$GeospatialPoseResult { *; } +# androidx.xr.arcore.runtime.Plane is referenced by native code. +-keep class androidx.xr.arcore.runtime.Plane { *; } +-keep class androidx.xr.arcore.runtime.Plane$Label { *; } +-keep class androidx.xr.arcore.runtime.Plane$Label$Companion { *; } \ No newline at end of file diff --git a/xr/runtime/runtime/proguard-rules.pro b/xr/runtime/runtime/proguard-rules.pro index b08b20cc59e80..51e72915a75a3 100644 --- a/xr/runtime/runtime/proguard-rules.pro +++ b/xr/runtime/runtime/proguard-rules.pro @@ -1,10 +1,40 @@ -# Prevent the Runtime, Internal, and Math classes from being obfuscated as they -# are created from native code. --keep class androidx.xr.runtime.** { *; } --keep class androidx.xr.runtime.**$* { *; } --keep class androidx.xr.runtime.math.** { *; } --keep class androidx.xr.runtime.math.**$* { *; } --keep class * extends androidx.xr.runtime.** { *; } --keep class * extends androidx.xr.runtime.**$* { *; } --keep class * extends androidx.xr.runtime.math.** { *; } --keep class * extends androidx.xr.runtime.math.**$* { *; } +# androidx.xr.runtime.DisplayBlendMode is referenced by native code. +-keep class androidx.xr.runtime.DisplayBlendMode { *; } +-keep class androidx.xr.runtime.DisplayBlendMode$Companion { *; } +# androidx.xr.runtime.FieldOfView is referenced by native code. +-keep class androidx.xr.runtime.FieldOfView { *; } +# androidx.xr.runtime.Log is referenced by native code. +-keep class androidx.xr.runtime.Log { *; } +# androidx.xr.runtime.TrackingState is referenced by native code. +-keep class androidx.xr.runtime.TrackingState { *; } +-keep class androidx.xr.runtime.TrackingState$Companion { *; } +# androidx.xr.runtime.math.FloatSize2d is referenced by native code. +-keep class androidx.xr.runtime.math.FloatSize2d { *; } +# androidx.xr.runtime.math.FloatSize3d is referenced by native code. +-keep class androidx.xr.runtime.math.FloatSize3d { *; } +# androidx.xr.runtime.math.GeospatialPose is referenced by native code. +-keep class androidx.xr.runtime.math.GeospatialPose { *; } +# androidx.xr.runtime.math.IntSize2d is referenced by native code. +-keep class androidx.xr.runtime.math.IntSize2d { *; } +# androidx.xr.runtime.math.Pose is referenced by native code. +-keep class androidx.xr.runtime.math.Pose { *; } +# androidx.xr.runtime.math.Quaternion is referenced by native code. +-keep class androidx.xr.runtime.math.Quaternion { *; } +# androidx.xr.runtime.math.Vector2 is referenced by native code. +-keep class androidx.xr.runtime.math.Vector2 { *; } +# androidx.xr.runtime.math.Vector2 is referenced by native code. +-keep class androidx.xr.runtime.math.Vector3 { *; } + +# Preserve implementations of the various factory interfaces, as these are +# instantiated via reflection and not directly. +-keep class androidx.xr.runtime.internal.PerceptionRuntimeFactory { *; } +-keep class * implements androidx.xr.runtime.internal.PerceptionRuntimeFactory { *; } +-keep class androidx.xr.runtime.internal.RenderingRuntimeFactory { *; } +-keep class * implements androidx.xr.runtime.internal.RenderingRuntimeFactory { *; } +-keep class androidx.xr.runtime.internal.SceneRuntimeFactory { *; } +-keep class * implements androidx.xr.runtime.internal.SceneRuntimeFactory { *; } + +# Preserve StateExtender and its implementations, as they're looked up via +# a service locator. +-keep class androidx.xr.runtime.StateExtender { *; } +-keep class * implements androidx.xr.runtime.StateExtender { *; } \ No newline at end of file diff --git a/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/MovableComponentImpl.kt b/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/MovableComponentImpl.kt index 9ae2b7548464a..5a758032d4288 100644 --- a/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/MovableComponentImpl.kt +++ b/xr/scenecore/scenecore-spatial-core/src/main/java/androidx/xr/scenecore/spatial/core/MovableComponentImpl.kt @@ -28,6 +28,7 @@ import androidx.xr.scenecore.runtime.InputEventListener import androidx.xr.scenecore.runtime.MovableComponent import androidx.xr.scenecore.runtime.MoveEvent import androidx.xr.scenecore.runtime.MoveEventListener +import androidx.xr.scenecore.runtime.PanelEntity import androidx.xr.scenecore.runtime.Space import androidx.xr.scenecore.spatial.core.RuntimeUtils.getPose import androidx.xr.scenecore.spatial.core.RuntimeUtils.getVector3 @@ -219,8 +220,8 @@ internal class MovableComponentImpl( .setEnabledReform(reformOptions.enabledReform or ReformOptions.ALLOW_MOVE) .scaleWithDistanceMode = translateScaleWithDistanceMode(scaleWithDistanceMode) - // TODO: b/348037292 - Remove this special case for PanelEntityImpl. - if (entity is PanelEntityImpl) { + // TODO: b/348037292 - Remove this special case for PanelEntity. + if (entity is PanelEntity) { size = entity.size } diff --git a/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/MovableComponentImplTest.kt b/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/MovableComponentImplTest.kt index 2ce0cfe9ae843..387bd35611672 100644 --- a/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/MovableComponentImplTest.kt +++ b/xr/scenecore/scenecore-spatial-core/src/test/java/androidx/xr/scenecore/spatial/core/MovableComponentImplTest.kt @@ -491,6 +491,50 @@ class MovableComponentImplTest { Truth.assertThat(options.currentSize.z).isEqualTo(2f) } + @Test + fun addMovableComponentToPanelEntity_updatesComponentSize() { + val panelEntity = createTestPanelEntity() + val movableComponent = + MovableComponentImpl( + systemMovable = true, + scaleInZ = true, + userAnchorable = false, + activitySpaceImpl = activitySpaceImpl, + panelShadowRenderer = mockPanelShadowRenderer, + runtimeExecutor = fakeExecutor, + ) + + // Initial size of movableComponent should be (0, 0, 0) + Truth.assertThat(movableComponent.size).isEqualTo(Dimensions(0f, 0f, 0f)) + + Truth.assertThat(panelEntity.addComponent(movableComponent)).isTrue() + + // After attaching, size should match entity size + Truth.assertThat(movableComponent.size).isEqualTo(panelEntity.size) + } + + @Test + fun addMovableComponentToMainPanelEntity_updatesComponentSize() { + val mainPanelEntity = sceneRuntime.mainPanelEntity + val movableComponent = + MovableComponentImpl( + systemMovable = true, + scaleInZ = true, + userAnchorable = false, + activitySpaceImpl = activitySpaceImpl, + panelShadowRenderer = mockPanelShadowRenderer, + runtimeExecutor = fakeExecutor, + ) + + // Initial size of movableComponent should be (0, 0, 0) + Truth.assertThat(movableComponent.size).isEqualTo(Dimensions(0f, 0f, 0f)) + + Truth.assertThat(mainPanelEntity.addComponent(movableComponent)).isTrue() + + // After attaching, size should match entity size + Truth.assertThat(movableComponent.size).isEqualTo(mainPanelEntity.size) + } + @Test fun scaleWithDistanceOnMovableComponent_defaultsToDefaultMode() { val entity = createTestEntity()