Merge "[Unit test lib] Add `runGlanceAppWidgetUnitTest` similar to compose's `runComposeUiTest` that provides scoped functions such as `provideContent`, `onNode` etc. to be able to write unit tests." into androidx-main
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 0492be7..d65c0f9 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -191,7 +191,9 @@
docs(project(":fragment:fragment-testing"))
docs(project(":glance:glance"))
docs(project(":glance:glance-appwidget"))
+ docs(project(":glance:glance-appwidget-testing"))
samples(project(":glance:glance-appwidget:glance-appwidget-samples"))
+ samples(project(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
docs(project(":glance:glance-appwidget-preview"))
docs(project(":glance:glance-preview"))
docs(project(":glance:glance-testing"))
diff --git a/glance/glance-appwidget-testing/api/current.txt b/glance/glance-appwidget-testing/api/current.txt
new file mode 100644
index 0000000..4c77ba8
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/current.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.testing.unit {
+
+ public sealed interface GlanceAppWidgetUnitTest extends androidx.glance.testing.GlanceNodeAssertionsProvider<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> {
+ method public void awaitIdle();
+ method public void provideComposable(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void setAppWidgetSize(long size);
+ method public void setContext(android.content.Context context);
+ method public <T> void setState(T state);
+ }
+
+ public final class GlanceAppWidgetUnitTestDefaults {
+ method public androidx.glance.GlanceId glanceId();
+ method public int hostCategory();
+ method public long size();
+ field public static final androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTestDefaults INSTANCE;
+ }
+
+ public final class GlanceAppWidgetUnitTestKt {
+ method public static void runGlanceAppWidgetUnitTest(optional long timeout, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTest,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/glance/glance-appwidget-testing/api/res-current.txt b/glance/glance-appwidget-testing/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/res-current.txt
diff --git a/glance/glance-appwidget-testing/api/restricted_current.txt b/glance/glance-appwidget-testing/api/restricted_current.txt
new file mode 100644
index 0000000..4c77ba8
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/restricted_current.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.testing.unit {
+
+ public sealed interface GlanceAppWidgetUnitTest extends androidx.glance.testing.GlanceNodeAssertionsProvider<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> {
+ method public void awaitIdle();
+ method public void provideComposable(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void setAppWidgetSize(long size);
+ method public void setContext(android.content.Context context);
+ method public <T> void setState(T state);
+ }
+
+ public final class GlanceAppWidgetUnitTestDefaults {
+ method public androidx.glance.GlanceId glanceId();
+ method public int hostCategory();
+ method public long size();
+ field public static final androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTestDefaults INSTANCE;
+ }
+
+ public final class GlanceAppWidgetUnitTestKt {
+ method public static void runGlanceAppWidgetUnitTest(optional long timeout, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTest,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/glance/glance-appwidget-testing/build.gradle b/glance/glance-appwidget-testing/build.gradle
new file mode 100644
index 0000000..8d5db0b
--- /dev/null
+++ b/glance/glance-appwidget-testing/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesTest)
+ api(project(":glance:glance-testing"))
+ api(project(":glance:glance-appwidget"))
+
+ testImplementation("androidx.core:core:1.7.0")
+ testImplementation("androidx.core:core-ktx:1.7.0")
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+
+ samples(projectOrArtifact(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
+}
+
+android {
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 23
+ }
+ namespace "androidx.glance.appwidget.testing"
+}
+
+androidx {
+ name = "androidx.glance:glance-appwidget-testing"
+ type = LibraryType.PUBLISHED_LIBRARY
+ targetsJavaConsumers = false
+ inceptionYear = "2023"
+ description = "This library provides APIs for developers to use for testing their appWidget specific Glance composables."
+}
diff --git a/glance/glance-appwidget-testing/samples/build.gradle b/glance/glance-appwidget-testing/samples/build.gradle
new file mode 100644
index 0000000..a5da7fa1
--- /dev/null
+++ b/glance/glance-appwidget-testing/samples/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ compileOnly(project(":annotation:annotation-sampled"))
+
+ implementation(project(":glance:glance"))
+ implementation(project(":glance:glance-testing"))
+ implementation(project(":glance:glance-appwidget-testing"))
+
+ implementation(libs.junit)
+ implementation(libs.testCore)
+ implementation("androidx.core:core:1.7.0")
+ implementation("androidx.core:core-ktx:1.7.0")
+}
+
+androidx {
+ name = "Glance AppWidget Testing Samples"
+ type = LibraryType.SAMPLES
+ targetsJavaConsumers = false
+ inceptionYear = "2023"
+ description = "Contains the sample code for testing the Glance AppWidget Composables"
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ }
+ namespace "androidx.glance.appwidget.testing.samples"
+}
diff --git a/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt b/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt
new file mode 100644
index 0000000..28a29c2
--- /dev/null
+++ b/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.width
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import org.junit.Test
+
+@Sampled
+@Suppress("unused")
+fun isolatedGlanceComposableTestSamples() {
+ class TestSample {
+ @Test
+ fun statusContent_statusFalse_outputsPending() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("status-text"))
+ .assert(hasText("Pending"))
+ }
+
+ @Test
+ fun statusContent_statusTrue_outputsFinished() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ StatusRow(
+ status = true
+ )
+ }
+
+ onNode(hasTestTag("status-text"))
+ .assert(hasText("Finished"))
+ }
+
+ @Test
+ fun header_smallSize_showsShortHeaderText() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 50.dp, height = 100.dp))
+
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("header-text"))
+ .assert(hasText("MyApp"))
+ }
+
+ @Test
+ fun header_largeSize_showsLongHeaderText() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 150.dp, height = 100.dp))
+
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("header-text"))
+ .assert(hasText("MyApp (Last order)"))
+ }
+
+ @Composable
+ fun WidgetContent(status: Boolean) {
+ Column {
+ Header()
+ Spacer()
+ StatusRow(status)
+ }
+ }
+
+ @Composable
+ fun Header() {
+ val width = LocalSize.current.width
+ Row(modifier = GlanceModifier.fillMaxSize()) {
+ Text(
+ text = if (width > 50.dp) {
+ "MyApp (Last order)"
+ } else {
+ "MyApp"
+ },
+ modifier = GlanceModifier.semantics { testTag = "header-text" }
+ )
+ }
+ }
+
+ @Composable
+ fun StatusRow(status: Boolean) {
+ Row(modifier = GlanceModifier.fillMaxSize()) {
+ Text(
+ text = "Status",
+ )
+ Spacer(modifier = GlanceModifier.width(10.dp))
+ Text(
+ text = if (status) {
+ "Pending"
+ } else {
+ "Finished"
+ },
+ modifier = GlanceModifier.semantics { testTag = "status-text" }
+ )
+ }
+ }
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt
new file mode 100644
index 0000000..c6282eb
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceId
+import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.state.GlanceStateDefinition
+import androidx.glance.testing.GlanceNodeAssertionsProvider
+import androidx.glance.testing.unit.GlanceMappedNode
+import androidx.glance.testing.unit.MappedNode
+import kotlin.time.Duration
+
+/**
+ * Sets up the test environment and runs the given unit [test block][block]. Use the methods on
+ * [GlanceAppWidgetUnitTest] in the test to provide Glance composable content, find Glance elements
+ * and make assertions on them.
+ *
+ * Test your individual Glance composable functions in isolation to verify that your logic outputs
+ * right elements. For example: if input data is 'x', an image 'y' was
+ * outputted. In sample below, the test class has a separate test for the header and the status
+ * row.
+ *
+ * Tests can be run on JVM as these don't involve rendering the UI. If your logic depends on
+ * [Context] or other android APIs, tests can be run on Android unit testing frameworks such as
+ * [Robolectric](https://github.com/robolectric/robolectric).
+ *
+ * Note: Keeping a reference to the [GlanceAppWidgetUnitTest] outside of this function is an error.
+ *
+ * @sample androidx.glance.appwidget.testing.samples.isolatedGlanceComposableTestSamples
+ *
+ * @param timeout test time out; defaults to 10s
+ * @param block The test block that involves calling methods in [GlanceAppWidgetUnitTest]
+ */
+// This and backing environment is based on pattern followed by
+// "androidx.compose.ui.test.runComposeUiTest". Alternative of exposing testRule was explored, but
+// it wasn't necessary for this case. If developers wish, they may use this function to create their
+// own test rule.
+fun runGlanceAppWidgetUnitTest(
+ timeout: Duration = DEFAULT_TIMEOUT,
+ block: GlanceAppWidgetUnitTest.() -> Unit
+) = GlanceAppWidgetUnitTestEnvironment(timeout).runTest(block)
+
+/**
+ * Provides methods to enable you to test your logic of building Glance composable content in the
+ * [runGlanceAppWidgetUnitTest] scope.
+ *
+ * @see [runGlanceAppWidgetUnitTest]
+ */
+sealed interface GlanceAppWidgetUnitTest :
+ GlanceNodeAssertionsProvider<MappedNode, GlanceMappedNode> {
+ /**
+ * Sets the size of the appWidget to be assumed for the test. This corresponds to the
+ * `LocalSize.current` composition local. If you are accessing the local size, you must
+ * call this method to set the intended size for the test.
+ *
+ * Note: This should be called before calling [provideComposable].
+ * Default is `349.dp, 455.dp` that of a 5x4 widget in Pixel 4 portrait mode. See
+ * [GlanceAppWidgetUnitTestDefaults.size]
+ *
+ * 1. If your appWidget uses `sizeMode == Single`, you can set this to the `minWidth` and
+ * `minHeight` set in your appwidget info xml.
+ * 2. If your appWidget uses `sizeMode == Exact`, you can identify the sizes to test looking
+ * at the documentation on
+ * [Determine a size for your widget](https://developer.android.com/develop/ui/views/appwidgets/layouts#anatomy_determining_size).
+ * and identifying landscape and portrait sizes that your widget may appear on.
+ * 3. If your appWidget uses `sizeMode == Responsive`, you can set this to one of the sizes from
+ * the list that you provide when specifying the sizeMode.
+ */
+ fun setAppWidgetSize(size: DpSize)
+
+ /**
+ * Sets the state to be used for the test if your composable under test accesses it via
+ * `currentState<*>()` or `LocalState.current`.
+ *
+ * Default state is `null`. Note: This should be called before calling [provideComposable],
+ * updates to the state after providing content has no effect. This matches the appWidget
+ * behavior where you need to call `update` on the widget for state changes to take effect.
+ *
+ * @param state the state to be used for testing the composable.
+ * @param T type of state used in your [GlanceStateDefinition] e.g. `Preferences` if your state
+ * definition is `GlanceStateDefinition<Preferences>`
+ */
+ fun <T> setState(state: T)
+
+ /**
+ * Sets the context to be used for the test.
+ *
+ * It is optional to call this method. However, you must set this if your composable needs
+ * access to `LocalContext`. You may need to use a Android unit test framework such as
+ * [Robolectric](https://github.com/robolectric/robolectric) to get the context.
+ *
+ * Note: This should be called before calling [provideComposable], updates to the state after
+ * providing content has no effect
+ */
+ fun setContext(context: Context)
+
+ /**
+ * Sets the Glance composable function to be tested. Each unit test should test a composable in
+ * isolation and assume specific state as input. Prefer keeping composables side-effects free.
+ * Perform any state changes needed for the test before calling [provideComposable] or
+ * [runGlanceAppWidgetUnitTest].
+ *
+ * @param composable the composable function under test
+ */
+ fun provideComposable(composable: @Composable () -> Unit)
+
+ /**
+ * Wait until all recompositions are calculated. For example if you have `LaunchedEffect` with
+ * delays in your composable.
+ */
+ fun awaitIdle()
+}
+
+/**
+ * Provides default values for various properties used in the Glance appWidget unit tests.
+ */
+object GlanceAppWidgetUnitTestDefaults {
+ /**
+ * [GlanceId] that can be assumed for state updates testing a Glance composable in isolation.
+ */
+ fun glanceId(): GlanceId = AppWidgetId(1)
+
+ /**
+ * Default size of the appWidget assumed in the unit tests. To override the size, use the
+ * [GlanceAppWidgetUnitTest.setAppWidgetSize] function.
+ *
+ * The default `349.dp, 455.dp` is that of a 5x4 widget in Pixel 4 portrait mode.
+ */
+ fun size(): DpSize = DpSize(height = 349.dp, width = 455.dp)
+
+ /**
+ * Default category of the appWidget assumed in the unit tests.
+ *
+ * The default is `WIDGET_CATEGORY_HOME_SCREEN`
+ */
+ fun hostCategory(): Int = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+}
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
new file mode 100644
index 0000000..674c8c5
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.Recomposer
+import androidx.compose.ui.unit.DpSize
+import androidx.glance.Applier
+import androidx.glance.LocalContext
+import androidx.glance.LocalGlanceId
+import androidx.glance.LocalSize
+import androidx.glance.LocalState
+import androidx.glance.appwidget.LocalAppWidgetOptions
+import androidx.glance.appwidget.RemoteViewsRoot
+import androidx.glance.session.globalSnapshotMonitor
+import androidx.glance.testing.GlanceNodeAssertion
+import androidx.glance.testing.GlanceNodeMatcher
+import androidx.glance.testing.TestContext
+import androidx.glance.testing.unit.GlanceMappedNode
+import androidx.glance.testing.unit.MappedNode
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+
+internal val DEFAULT_TIMEOUT = 10.seconds
+
+/**
+ * An implementation of [GlanceAppWidgetUnitTest] that provides APIs to run composition for
+ * appwidget-specific Glance composable content.
+ */
+internal class GlanceAppWidgetUnitTestEnvironment(
+ private val timeout: Duration
+) : GlanceAppWidgetUnitTest {
+ private var testContext = TestContext<MappedNode, GlanceMappedNode>()
+ private var testScope = TestScope()
+
+ // Data for composition locals
+ private var context: Context? = null
+ private val fakeGlanceID = GlanceAppWidgetUnitTestDefaults.glanceId()
+ private var size: DpSize = GlanceAppWidgetUnitTestDefaults.size()
+ private var state: Any? = null
+
+ private val root = RemoteViewsRoot(10)
+
+ private lateinit var recomposer: Recomposer
+ private lateinit var composition: Composition
+
+ fun runTest(block: GlanceAppWidgetUnitTest.() -> Unit) = testScope.runTest(timeout) {
+ var snapshotMonitor: Job? = null
+ try {
+ // GlobalSnapshotManager.ensureStarted() uses Dispatcher.Default, so using
+ // globalSnapshotMonitor instead to be able to use test dispatcher instead.
+ snapshotMonitor = launch { globalSnapshotMonitor() }
+ val applier = Applier(root)
+ recomposer = Recomposer(testScope.coroutineContext)
+ composition = Composition(applier, recomposer)
+ block()
+ } finally {
+ composition.dispose()
+ snapshotMonitor?.cancel()
+ recomposer.cancel()
+ recomposer.join()
+ }
+ }
+
+ // Among the appWidgetOptions available, size related options shouldn't generally be necessary
+ // for developers to look up - the LocalSize composition local should suffice. So, currently, we
+ // only initialize host category.
+ private val appWidgetOptions = Bundle().apply {
+ putInt(
+ AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
+ GlanceAppWidgetUnitTestDefaults.hostCategory()
+ )
+ }
+
+ override fun provideComposable(composable: @Composable () -> Unit) {
+ check(testContext.rootGlanceNode == null) {
+ "provideComposable can only be called once"
+ }
+
+ testScope.launch {
+ var compositionLocals = arrayOf(
+ LocalGlanceId provides fakeGlanceID,
+ LocalState provides state,
+ LocalAppWidgetOptions provides appWidgetOptions,
+ LocalSize provides size
+ )
+ context?.let {
+ compositionLocals = compositionLocals.plus(LocalContext provides it)
+ }
+
+ composition.setContent {
+ CompositionLocalProvider(
+ values = compositionLocals,
+ content = composable,
+ )
+ }
+
+ launch(currentCoroutineContext() + TestFrameClock()) {
+ recomposer.runRecomposeAndApplyChanges()
+ }
+
+ launch {
+ recomposer.currentState.collect { curState ->
+ when (curState) {
+ Recomposer.State.Idle -> {
+ testContext.rootGlanceNode = GlanceMappedNode(
+ emittable = root.copy()
+ )
+ }
+
+ Recomposer.State.ShutDown -> {
+ cancel()
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ override fun awaitIdle() {
+ testScope.testScheduler.advanceUntilIdle()
+ }
+
+ override fun onNode(
+ matcher: GlanceNodeMatcher<MappedNode>
+ ): GlanceNodeAssertion<MappedNode, GlanceMappedNode> {
+ // Always let all the enqueued tasks finish before inspecting the tree.
+ testScope.testScheduler.runCurrent()
+ // Calling onNode resets the previously matched nodes and starts a new matching chain.
+ testContext.reset()
+ // Delegates matching to the next assertion.
+ return GlanceNodeAssertion(matcher, testContext)
+ }
+
+ override fun setAppWidgetSize(size: DpSize) {
+ check(testContext.rootGlanceNode == null) {
+ "setApWidgetSize should be called before calling provideComposable"
+ }
+ this.size = size
+ }
+
+ override fun <T> setState(state: T) {
+ check(testContext.rootGlanceNode == null) {
+ "setState should be called before calling provideComposable"
+ }
+ this.state = state
+ }
+
+ override fun setContext(context: Context) {
+ check(testContext.rootGlanceNode == null) {
+ "setContext should be called before calling provideComposable"
+ }
+ this.context = context
+ }
+
+ /**
+ * Test clock that sends all frames immediately.
+ */
+ // Same as TestUtils.TestFrameClock used in Glance unit tests.
+ private class TestFrameClock : MonotonicFrameClock {
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R) =
+ onFrame(System.currentTimeMillis())
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/test/AndroidManifest.xml b/glance/glance-appwidget-testing/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..f125c7b
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest>
+ <application/>
+</manifest>
\ No newline at end of file
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt
new file mode 100644
index 0000000..514826d
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.testing.test.R
+import androidx.glance.layout.Column
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [33])
+/**
+ * Holds tests that use Robolectric for providing application resources and context.
+ */
+class GlanceAppWidgetUnitTestEnvironmentRobolectricTest {
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ }
+
+ @Test
+ fun runTest_localContextRead() = runGlanceAppWidgetUnitTest {
+ setContext(context)
+
+ provideComposable {
+ ComposableReadingLocalContext()
+ }
+
+ onNode(hasTestTag("test-tag"))
+ .assert(hasText("Test string: MyTest"))
+ }
+
+ @Composable
+ fun ComposableReadingLocalContext() {
+ val context = LocalContext.current
+
+ Column {
+ Text(
+ text = "Test string: ${context.getString(R.string.glance_test_string)}",
+ modifier = GlanceModifier.semantics { testTag = "test-tag" }
+ )
+ }
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
new file mode 100644
index 0000000..a1449d8
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.preferencesOf
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.ImageProvider
+import androidx.glance.appwidget.testing.test.R
+import androidx.glance.currentState
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import kotlinx.coroutines.delay
+import org.junit.Test
+
+// In this test we aren't specifically testing anything bound to SDK, so we can run it without
+// android unit test runners such as Robolectric.
+class GlanceAppWidgetUnitTestEnvironmentTest {
+ @Test
+ fun runTest_localSizeRead() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 120.dp, height = 200.dp))
+
+ provideComposable {
+ ComposableReadingLocalSize()
+ }
+
+ onNode(hasText("120.0 dp x 200.0 dp")).assertExists()
+ }
+
+ @Composable
+ fun ComposableReadingLocalSize() {
+ val size = LocalSize.current
+ Column {
+ Text(text = "${size.width.value} dp x ${size.height.value} dp")
+ Spacer()
+ Image(
+ provider = ImageProvider(R.drawable.glance_test_android),
+ contentDescription = "test-image",
+ )
+ }
+ }
+
+ @Test
+ fun runTest_currentStateRead() = runGlanceAppWidgetUnitTest {
+ setState(preferencesOf(toggleKey to true))
+
+ provideComposable {
+ ComposableReadingState()
+ }
+
+ onNode(hasText("isToggled")).assertExists()
+ }
+
+ @Composable
+ fun ComposableReadingState() {
+ Column {
+ Text(text = "A text")
+ Spacer()
+ Text(text = getTitle(currentState<Preferences>()[toggleKey] == true))
+ Spacer()
+ Image(
+ provider = ImageProvider(R.drawable.glance_test_android),
+ contentDescription = "test-image",
+ modifier = GlanceModifier.semantics { testTag = "img" }
+ )
+ }
+ }
+
+ @Test
+ fun runTest_onNodeCalledMultipleTimes() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ Text(text = "abc")
+ Spacer()
+ Text(text = "xyz")
+ }
+
+ onNode(hasText("abc")).assertExists()
+ // test context reset and new filter matched onNode
+ onNode(hasText("xyz")).assertExists()
+ onNode(hasText("def")).assertDoesNotExist()
+ }
+
+ @Test
+ fun runTest_effect() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ text = "changed"
+ }
+ }
+
+ onNode(hasTestTag("mutable-test")).assert(hasText("changed"))
+ }
+
+ @Test
+ fun runTest_effectWithDelay() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ delay(100L)
+ text = "changed"
+ }
+ }
+
+ awaitIdle() // Since the launched effect has a delay.
+ onNode(hasTestTag("mutable-test")).assert(hasText("changed"))
+ }
+
+ @Test
+ fun runTest_effectWithDelayWithoutAdvancing() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ delay(100L)
+ text = "changed"
+ }
+ }
+
+ onNode(hasTestTag("mutable-test")).assert(hasText("initial"))
+ }
+}
+
+private val toggleKey = booleanPreferencesKey("title_toggled_key")
+private fun getTitle(toggled: Boolean) = if (toggled) "isToggled" else "notToggled"
diff --git a/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml b/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml
new file mode 100644
index 0000000..49a3142
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector android:alpha="0.9" android:height="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"/>
+</vector>
diff --git a/glance/glance-appwidget-testing/src/test/res/values/strings.xml b/glance/glance-appwidget-testing/src/test/res/values/strings.xml
new file mode 100644
index 0000000..88a0850
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="glance_test_string">MyTest</string>
+</resources>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index f667413..a1d03ba 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -23,6 +23,8 @@
import android.util.Log
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.compose.runtime.Composable
import androidx.glance.GlanceComposable
import androidx.glance.GlanceId
@@ -194,7 +196,8 @@
}
}
-internal data class AppWidgetId(val appWidgetId: Int) : GlanceId
+@RestrictTo(Scope.LIBRARY_GROUP)
+data class AppWidgetId(val appWidgetId: Int) : GlanceId
/** Update all App Widgets managed by the [GlanceAppWidget] class. */
suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
index 1428527..df0e178 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
@@ -16,6 +16,8 @@
package androidx.glance.appwidget
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.glance.Emittable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceModifier
@@ -24,7 +26,8 @@
* Root view, with a maximum depth. No default value is specified, as the exact value depends on
* specific circumstances.
*/
-internal class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
+@RestrictTo(Scope.LIBRARY_GROUP)
+ class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
override var modifier: GlanceModifier = GlanceModifier
override fun copy(): Emittable = RemoteViewsRoot(maxDepth).also {
it.modifier = modifier
diff --git a/glance/glance-testing/api/current.txt b/glance/glance-testing/api/current.txt
index 51dd245..e46a742 100644
--- a/glance/glance-testing/api/current.txt
+++ b/glance/glance-testing/api/current.txt
@@ -14,6 +14,10 @@
method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
}
+ public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+ method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+ }
+
public final class GlanceNodeMatcher<R> {
ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
diff --git a/glance/glance-testing/api/restricted_current.txt b/glance/glance-testing/api/restricted_current.txt
index 51dd245..e46a742 100644
--- a/glance/glance-testing/api/restricted_current.txt
+++ b/glance/glance-testing/api/restricted_current.txt
@@ -14,6 +14,10 @@
method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
}
+ public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+ method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+ }
+
public final class GlanceNodeMatcher<R> {
ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
new file mode 100644
index 0000000..624b529
--- /dev/null
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.testing
+
+/**
+ * Provides an entry point into testing exposing methods to find glance nodes
+ */
+// Equivalent to "androidx.compose.ui.test.SemanticsNodeInteractionsProvider" from compose.
+interface GlanceNodeAssertionsProvider<R, T : GlanceNode<R>> {
+ /**
+ * Finds a Glance node that matches the given condition.
+ *
+ * Any subsequent operation on its result will expect exactly one element found and will throw
+ * [AssertionError] if none or more than one element is found.
+ *
+ * @param matcher Matcher used for filtering
+ */
+ fun onNode(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertion<R, T>
+}
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
index 1399349..1ab76a3 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
@@ -23,6 +23,13 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class TestContext<R, T : GlanceNode<R>> {
+ /**
+ * To be called on every onNode to restart matching and clear cache.
+ */
+ fun reset() {
+ cachedMatchedNodes = emptyList()
+ }
+
var rootGlanceNode: T? = null
var cachedMatchedNodes: List<GlanceNode<R>> = emptyList()
}
diff --git a/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt b/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
index cebf899..d5f6d8d 100644
--- a/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
@@ -17,6 +17,7 @@
package androidx.glance.session
import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.compose.runtime.snapshots.Snapshot
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
@@ -33,7 +34,7 @@
* state changes). These will be sent on Dispatchers.Default.
* This is based on [androidx.compose.ui.platform.GlobalSnapshotManager].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY_GROUP)
object GlobalSnapshotManager {
private val started = AtomicBoolean(false)
private val sent = AtomicBoolean(false)
@@ -59,7 +60,8 @@
/**
* Monitors global snapshot state writes and sends apply notifications.
*/
-internal suspend fun globalSnapshotMonitor() {
+@RestrictTo(Scope.LIBRARY_GROUP)
+suspend fun globalSnapshotMonitor() {
val channel = Channel<Unit>(1)
val sent = AtomicBoolean(false)
val observerHandle = Snapshot.registerGlobalWriteObserver {
diff --git a/settings.gradle b/settings.gradle
index d897f49..7f6500a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -763,7 +763,9 @@
includeProject(":glance:glance-appwidget", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-preview", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-proto", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-testing", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:glance-appwidget-samples", "glance/glance-appwidget/samples", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-testing:glance-appwidget-testing-samples", "glance/glance-appwidget-testing/samples", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:demos", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark-target", [BuildType.GLANCE])