Merge "Reset pointer IDs after not accepting an input stream" into androidx-main
diff --git a/appsearch/appsearch-builtin-types/OWNERS b/appsearch/appsearch-builtin-types/OWNERS
new file mode 100644
index 0000000..e863f7d
--- /dev/null
+++ b/appsearch/appsearch-builtin-types/OWNERS
@@ -0,0 +1,4 @@
[email protected]
[email protected]
[email protected]
[email protected]
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index e377245..1e1b44d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -113,6 +113,7 @@
project.configureTaskTimeouts()
project.configureMavenArtifactUpload(extension)
+ project.configureExportLibraryGroupsToXml()
project.configureExternalDependencyLicenseCheck()
project.configureProjectStructureValidation(extension)
project.configureProjectVersionValidation(extension)
@@ -394,6 +395,17 @@
project.addToProjectMap(extension)
}
+ private fun Project.configureExportLibraryGroupsToXml() {
+ project.tasks.register(
+ "exportLibraryGroupsToXml",
+ ExportLibraryGroupsToXmlTask::class.java
+ ) { task ->
+ task.libraryGroupFile = project.file("${project.getSupportRootFolder()}" +
+ "/buildSrc/public/src/main/kotlin/androidx/build/LibraryGroups.kt")
+ task.xmlOutputFile = project.file("${project.buildDir}/lint/library-groups.xml")
+ }
+ }
+
private fun Project.configureProjectStructureValidation(
extension: AndroidXExtension
) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ExportLibraryGroupsToXmlTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/ExportLibraryGroupsToXmlTask.kt
new file mode 100644
index 0000000..bb8df4a
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ExportLibraryGroupsToXmlTask.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import com.google.common.io.Files
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import java.io.BufferedWriter
+import java.io.File
+import java.io.Writer
+import kotlin.reflect.full.memberProperties
+import kotlin.text.Charsets.UTF_8
+
+/**
+ * Task that parses the contents of a given library group file (usually [LibraryGroups]) and writes
+ * them to an XML file. The XML file is then used by Lint.
+ */
+@CacheableTask
+abstract class ExportLibraryGroupsToXmlTask : DefaultTask() {
+
+ @get:[InputFile PathSensitive(PathSensitivity.NONE)]
+ lateinit var libraryGroupFile: File
+
+ @get:OutputFile
+ lateinit var xmlOutputFile: File
+
+ @TaskAction
+ fun exec() {
+ val writer: Writer = BufferedWriter(Files.newWriter(xmlOutputFile, UTF_8))
+
+ // Write XML header and outermost opening tag
+ writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ writer.write("<libraryGroups>\n")
+
+ LibraryGroups::class.memberProperties.forEach { member ->
+ try {
+ val libraryGroup = member.get(LibraryGroups) as LibraryGroup
+ val groupName = libraryGroup.group
+ val isAtomic = (libraryGroup.forcedVersion != null)
+
+ // Write data for this LibraryGroup
+ writer.run {
+ write(" <libraryGroup>\n")
+ write(" <group>$groupName</group>\n")
+ write(" <isAtomic>$isAtomic</isAtomic>\n")
+ write(" </libraryGroup>\n")
+ }
+ } catch (ignore: ClassCastException) {
+ // Object isn't a LibraryGroup, skip it
+ }
+ }
+
+ // Write outermost closing tag and close writer
+ writer.write("<libraryGroups>\n")
+ writer.close()
+ }
+}
diff --git a/camera/camera-camera2-pipe/build.gradle b/camera/camera-camera2-pipe/build.gradle
index af4e291..3cdad88 100644
--- a/camera/camera-camera2-pipe/build.gradle
+++ b/camera/camera-camera2-pipe/build.gradle
@@ -43,7 +43,7 @@
testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.truth)
- testImplementation(libs.robolectric)
+ testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/205731854): fix tests to work with SDK 31 and robolectric 4.7
testImplementation(libs.kotlinCoroutinesTest)
testImplementation(project(":camera:camera-camera2-pipe-testing"))
testImplementation(project(":internal-testutils-truth"))
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 11c1c32..386a9c0 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -38,7 +38,7 @@
testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.truth)
- testImplementation(libs.robolectric)
+ testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/205731854): fix tests to work with SDK 31 and robolectric 4.7
testImplementation(libs.mockitoCore)
testImplementation(libs.kotlinCoroutinesTest)
testImplementation("androidx.annotation:annotation-experimental:1.1.0")
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
index 55deed1..8bb3a03 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
@@ -224,9 +224,9 @@
assertThat(tollCard.getCardState().getValue()).isEqualTo(TollCard.TOLLCARD_STATE_VALID);
}
- @Config(minSdk = 30)
+ @Config(maxSdk = 30)
@Test
- public void getTollCard_verifyResponseApi30() {
+ public void getTollCard_verifyResponseApi30() throws InterruptedException {
AtomicReference<TollCard> loadedResult = new AtomicReference<>();
OnCarDataAvailableListener<TollCard> listener = (data) -> {
loadedResult.set(data);
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
index 49995ca..49e75b3 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
@@ -54,6 +54,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlin.math.floor
+import kotlin.math.max
/**
* <a href="https://material.io/components/checkboxes" class="external" target="_blank">Material Design checkbox</a>.
@@ -322,7 +323,8 @@
boxColor,
topLeft = Offset(strokeWidth, strokeWidth),
size = Size(checkboxSize - strokeWidth * 2, checkboxSize - strokeWidth * 2),
- cornerRadius = CornerRadius(radius / 2),
+ // Set the inner radius to be equal to the outer radius - border's stroke width.
+ cornerRadius = CornerRadius(max(0f, radius - strokeWidth)),
style = Fill
)
drawRoundRect(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
new file mode 100644
index 0000000..67f6d1e
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performMouseInput
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTestApi::class)
+class IconButtonScreenshotTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+ private val wrap = Modifier.wrapContentSize(Alignment.TopStart)
+ private val wrapperTestTag = "iconButtonWrapper"
+
+ @Test
+ fun iconButton_lightTheme() {
+ rule.setMaterialContent {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_lightTheme")
+ }
+
+ @Test
+ fun iconButton_darkTheme() {
+ rule.setContent {
+ MaterialTheme(darkColorScheme()) {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ }
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_darkTheme")
+ }
+
+ @Test
+ fun iconButton_lightTheme_disabled() {
+
+ rule.setMaterialContent {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }, enabled = false) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ assertAgainstGolden("iconButton_lightTheme_disabled")
+ }
+
+ @Test
+ fun iconButton_lightTheme_pressed() {
+ rule.setMaterialContent {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+
+ rule.mainClock.autoAdvance = false
+ rule.onNode(hasClickAction())
+ .performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+ // synchronization. Instead just wait until after the ripples are finished animating.
+ Thread.sleep(300)
+
+ assertAgainstGolden("iconButton_lightTheme_pressed")
+ }
+
+ @Test
+ fun iconButton_lightTheme_hovered() {
+ rule.setMaterialContent {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ rule.onNodeWithTag(wrapperTestTag).performMouseInput {
+ enter(center)
+ }
+
+ assertAgainstGolden("iconButton_lightTheme_hovered")
+ }
+
+ @Test
+ fun iconButton_lightTheme_focused() {
+ val focusRequester = FocusRequester()
+
+ rule.setMaterialContent {
+ Box(wrap.testTag(wrapperTestTag)) {
+ IconButton(onClick = { /* doSomething() */ },
+ modifier = Modifier
+ // Normally this is only focusable in non-touch mode, so let's force it to
+ // always be focusable so we can test how it appears
+ .focusProperties { canFocus = true }
+ .focusRequester(focusRequester)
+ ) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ assertAgainstGolden("iconButton_lightTheme_focused")
+ }
+
+ private fun assertAgainstGolden(goldenName: String) {
+ rule.onNodeWithTag(wrapperTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenName)
+ }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
index ed666cb..13e1bf2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
@@ -55,6 +55,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlin.math.floor
+import kotlin.math.max
/**
* Material Design checkbox.
@@ -341,7 +342,8 @@
boxColor,
topLeft = Offset(strokeWidth, strokeWidth),
size = Size(checkboxSize - strokeWidth * 2, checkboxSize - strokeWidth * 2),
- cornerRadius = CornerRadius(radius / 2),
+ // Set the inner radius to be equal to the outer radius - border's stroke width.
+ cornerRadius = CornerRadius(max(0f, radius - strokeWidth)),
style = Fill
)
drawRoundRect(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index ba9daf7..cafed40 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -70,7 +70,10 @@
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
- indication = rememberRipple(bounded = false, radius = RippleRadius)
+ indication = rememberRipple(
+ bounded = false,
+ radius = IconButton.StateLayerSize / 2
+ )
),
contentAlignment = Alignment.Center
) {
@@ -121,7 +124,10 @@
enabled = enabled,
role = Role.Checkbox,
interactionSource = interactionSource,
- indication = rememberRipple(bounded = false, radius = RippleRadius)
+ indication = rememberRipple(
+ bounded = false,
+ radius = IconButton.StateLayerSize / 2
+ )
),
contentAlignment = Alignment.Center
) {
@@ -134,6 +140,3 @@
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
}
}
-
-// Default radius of an unbounded ripple in an IconButton
-private val RippleRadius = IconButton.StateLayerSize
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index f52e504..863a04a 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.tooling
import android.app.Activity
+import android.os.Build
import android.os.Bundle
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.ui.tooling.animation.PreviewAnimationClock
@@ -336,10 +337,14 @@
onDraw = { onDrawCounter++ }
)
}
+
+ // API before 22, might issue an additional draw under testing.
+ val expectedDrawCount = if (Build.VERSION.SDK_INT < 22) 2 else 1
repeat(5) {
activityTestRule.runOnUiThread {
assertEquals(1, compositionCount.get())
- assertTrue("At most, 1 draw is expected", onDrawCounter < 2)
+ assertTrue("At most, $expectedDrawCount draw is expected ($onDrawCounter happened)",
+ onDrawCounter <= expectedDrawCount)
}
Thread.sleep(250)
}
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index e1712fe..1e133f1 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -70,7 +70,7 @@
testImplementation(libs.truth)
testImplementation(libs.mockitoCore)
testImplementation(libs.mockitoKotlin)
- testImplementation(libs.robolectric)
+ testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/205731854): fix tests to work with SDK 31 and robolectric 4.7
testImplementation(project(":compose:ui:ui-test-junit4"))
testImplementation(project(":compose:test-utils"))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 9e39112..086de49 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -125,7 +125,7 @@
*
* @sample androidx.compose.ui.samples.ModifierParameterSample
*/
- // The companion object implements `Modifier` so that it may be used as the start of a
+ // The companion object implements `Modifier` so that it may be used as the start of a
// modifier extension factory expression.
companion object : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
diff --git a/core/OWNERS b/core/OWNERS
index cb02012..3a3ca6d 100644
--- a/core/OWNERS
+++ b/core/OWNERS
@@ -16,5 +16,5 @@
[email protected]
# For shortcut related files
[email protected]
[email protected]
[email protected]
diff --git a/core/core-performance/build.gradle b/core/core-performance/build.gradle
index add1f93..f64c5a7 100644
--- a/core/core-performance/build.gradle
+++ b/core/core-performance/build.gradle
@@ -26,6 +26,8 @@
dependencies {
api(libs.kotlinStdlib)
+
+ testImplementation(libs.testCore)
testImplementation(libs.kotlinStdlib)
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt b/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
index 246f23a..518ab09 100644
--- a/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
+++ b/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
@@ -16,12 +16,14 @@
package androidx.core.performance
+import android.content.Context
import android.os.Build
/**
* Reports the media performance class of the device.
+ * @param context ApplicationContext
*/
-class PerformanceClass {
+class PerformanceClass(private val context: Context) {
/**
* The media performance class of the device or 0 if none.
diff --git a/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt b/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
index 895b376..ab2b47b 100644
--- a/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
+++ b/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
@@ -16,8 +16,10 @@
package androidx.core.performance
+import android.app.Application
import android.os.Build.VERSION_CODES.R
import android.os.Build.VERSION_CODES.S
+import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,17 +32,17 @@
@RunWith(RobolectricTestRunner::class)
class PerformanceClassTest {
- private val pc = PerformanceClass()
-
@Test
@Config(maxSdk = R)
fun getMediaPerformanceClass_sdk30() {
+ val pc = createPerformanceClass()
assertThat(pc.getMediaPerformanceClass()).isEqualTo(0)
}
@Test
@Config(minSdk = S)
fun getMediaPerformanceClass_sdk31_declared30() {
+ val pc = createPerformanceClass()
// TODO(b/205732671): Use ShadowBuild.setMediaPerformanceClass when available
ShadowSystemProperties.override("ro.odm.build.media_performance_class", "30")
ShadowBuild.reset()
@@ -50,8 +52,13 @@
@Test
@Config(minSdk = S)
fun getMediaPerformanceClass_sdk31_notDeclared() {
+ val pc = createPerformanceClass()
// TODO(b/205732671): Use ShadowBuild.setMediaPerformanceClass when available
ShadowBuild.reset()
assertThat(pc.getMediaPerformanceClass()).isEqualTo(0)
}
+
+ private fun createPerformanceClass(): PerformanceClass {
+ return PerformanceClass(ApplicationProvider.getApplicationContext<Application>())
+ }
}
\ No newline at end of file
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 36287014..635400e3 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -224,6 +224,7 @@
location\: class PendingIntent
\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
# > Task :buildOnServer
+[0-9]+ problems were found reusing the configuration cache\.
[0-9]+ problems were found reusing the configuration cache, [0-9]+ of which seem unique\.
[0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ up\-to\-date
Configuration cache entry reused with [0-9]+ problems\.
@@ -610,4 +611,6 @@
Info: Methods with invalid locals information:
java\.lang\.Object androidx\.glance\.state\.GlanceState\.getDataStore\(android\.content\.Context, androidx\.glance\.state\.GlanceStateDefinition, java\.lang\.String, kotlin\.coroutines\.Continuation\)
Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint OBJECT\.
-Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
\ No newline at end of file
+Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
+# > Task :compose:ui:ui-inspection:buildCMakeRelWithDebInfo[arm64-v8a]
+C/C\+\+: ninja: warning: bad deps log signature or version; starting over
\ No newline at end of file
diff --git a/development/build_log_simplifier/update.sh b/development/build_log_simplifier/update.sh
index ef8ba07..e5b3a20 100755
--- a/development/build_log_simplifier/update.sh
+++ b/development/build_log_simplifier/update.sh
@@ -54,7 +54,12 @@
else
logName="gradle.${i}.log"
fi
- if fetch_artifact --bid "$buildId" --target "$target" "logs/$logName"; then
+ filepath="logs/$logName"
+ # incremental build uses a subdirectory
+ if [ "$target" == "androidx_incremental" ]; then
+ filepath="incremental/$filepath"
+ fi
+ if fetch_artifact --bid "$buildId" --target "$target" "$filepath"; then
echo "downloaded log ${i} in build $buildId target $target"
else
echo
diff --git a/development/project-creator/create_project.py b/development/project-creator/create_project.py
index 7501453..ae4b191 100755
--- a/development/project-creator/create_project.py
+++ b/development/project-creator/create_project.py
@@ -745,6 +745,10 @@
project_type = ProjectType.KOTLIN
else:
project_type = ask_project_type()
+ insert_new_group_id_into_library_versions_kt(
+ args.group_id,
+ args.artifact_id
+ )
create_directories(
args.group_id,
args.artifact_id,
@@ -752,8 +756,6 @@
is_compose_project(args.group_id, args.artifact_id)
)
update_settings_gradle(args.group_id, args.artifact_id)
- insert_new_group_id_into_library_versions_kt(args.group_id,
- args.artifact_id)
update_docs_tip_of_tree_build_grade(args.group_id, args.artifact_id)
print("Created directories. \nRunning updateApi for the new "
"library, this may take a minute...", end='')
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 2efcafe..302578a 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -54,12 +54,15 @@
method @androidx.compose.runtime.Composable public abstract void Content();
method public androidx.glance.appwidget.SizeMode getSizeMode();
method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
+ method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public androidx.glance.appwidget.SizeMode sizeMode;
property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
}
public final class GlanceAppWidgetKt {
+ method public static suspend Object? updateAll(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend inline <reified State> void updateIf(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, kotlin.jvm.functions.Function1<? super State,? extends java.lang.Boolean> predicate);
}
public final class GlanceAppWidgetManager {
@@ -183,7 +186,9 @@
public final class GlanceAppWidgetStateKt {
method public static suspend <T> Object? getAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? getAppWidgetState(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
method public static suspend <T> Object? updateAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? updateAppWidgetState(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
}
}
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 2efcafe..302578a 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -54,12 +54,15 @@
method @androidx.compose.runtime.Composable public abstract void Content();
method public androidx.glance.appwidget.SizeMode getSizeMode();
method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
+ method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public androidx.glance.appwidget.SizeMode sizeMode;
property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
}
public final class GlanceAppWidgetKt {
+ method public static suspend Object? updateAll(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend inline <reified State> void updateIf(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, kotlin.jvm.functions.Function1<? super State,? extends java.lang.Boolean> predicate);
}
public final class GlanceAppWidgetManager {
@@ -183,7 +186,9 @@
public final class GlanceAppWidgetStateKt {
method public static suspend <T> Object? getAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? getAppWidgetState(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
method public static suspend <T> Object? updateAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? updateAppWidgetState(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
}
}
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 2efcafe..302578a 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -54,12 +54,15 @@
method @androidx.compose.runtime.Composable public abstract void Content();
method public androidx.glance.appwidget.SizeMode getSizeMode();
method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
+ method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public androidx.glance.appwidget.SizeMode sizeMode;
property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
}
public final class GlanceAppWidgetKt {
+ method public static suspend Object? updateAll(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend inline <reified State> void updateIf(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, kotlin.jvm.functions.Function1<? super State,? extends java.lang.Boolean> predicate);
}
public final class GlanceAppWidgetManager {
@@ -183,7 +186,9 @@
public final class GlanceAppWidgetStateKt {
method public static suspend <T> Object? getAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? getAppWidgetState(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
method public static suspend <T> Object? updateAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? updateAppWidgetState(androidx.glance.appwidget.GlanceAppWidget, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
}
}
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
index 0a187b8..981a4cb 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
@@ -85,12 +85,15 @@
private val mInnerRules = RuleChain.outerRule(mActivityRule).around(mOrientationRule)
private var mHostStarted = false
- lateinit var mHostView: TestAppWidgetHostView
+ private var mMaybeHostView: TestAppWidgetHostView? = null
private var mAppWidgetId = 0
private val mScenario: ActivityScenario<AppWidgetHostTestActivity>
get() = mActivityRule.scenario
private val mContext = ApplicationProvider.getApplicationContext<Context>()
+ val mHostView: TestAppWidgetHostView
+ get() = checkNotNull(mMaybeHostView) { "No app widget installed on the host" }
+
override fun apply(base: Statement, description: Description) = object : Statement() {
override fun evaluate() {
@@ -111,12 +114,21 @@
mHostStarted = true
mActivityRule.scenario.onActivity { activity ->
- mHostView = activity.bindAppWidget(mPortraitSize, mLandscapeSize)
+ mMaybeHostView = activity.bindAppWidget(mPortraitSize, mLandscapeSize)
}
+ val hostView = checkNotNull(mMaybeHostView) { "Host view wasn't successfully started" }
+
runAndWaitForChildren {
- mAppWidgetId = mHostView.appWidgetId
- mHostView.waitForRemoteViews()
+ mAppWidgetId = hostView.appWidgetId
+ hostView.waitForRemoteViews()
+ }
+ }
+
+ fun removeAppWidget() {
+ mActivityRule.scenario.onActivity { activity ->
+ val hostView = checkNotNull(mMaybeHostView) { "No app widget to remove" }
+ activity.deleteAppWidget(hostView)
}
}
@@ -182,18 +194,21 @@
mPortraitSize = portrait
if (!mHostStarted) return
- mScenario.onActivity {
- mHostView.setSizes(portrait, landscape)
- }
+ val hostView = mMaybeHostView
+ if (hostView != null) {
+ mScenario.onActivity {
+ hostView.setSizes(portrait, landscape)
+ }
- if (updateRemoteViews) {
- runAndWaitForChildren {
- mHostView.resetRemoteViewsLatch()
- AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(
- mAppWidgetId,
- optionsBundleOf(listOf(portrait, landscape))
- )
- mHostView.waitForRemoteViews()
+ if (updateRemoteViews) {
+ runAndWaitForChildren {
+ hostView.resetRemoteViewsLatch()
+ AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(
+ mAppWidgetId,
+ optionsBundleOf(listOf(portrait, landscape))
+ )
+ hostView.waitForRemoteViews()
+ }
}
}
}
@@ -203,12 +218,13 @@
run: () -> Unit = {},
test: () -> Boolean
) {
+ val hostView = mHostView
val latch = CountDownLatch(1)
val onDrawListener = ViewTreeObserver.OnDrawListener {
- if (mHostView.childCount > 0 && test()) latch.countDown()
+ if (hostView.childCount > 0 && test()) latch.countDown()
}
mActivityRule.scenario.onActivity {
- mHostView.viewTreeObserver.addOnDrawListener(onDrawListener)
+ hostView.viewTreeObserver.addOnDrawListener(onDrawListener)
}
run()
@@ -224,7 +240,7 @@
} finally {
latch.countDown() // make sure it's released in all conditions
mActivityRule.scenario.onActivity {
- mHostView.viewTreeObserver.removeOnDrawListener(onDrawListener)
+ hostView.viewTreeObserver.removeOnDrawListener(onDrawListener)
}
}
}
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
index a5b1fab..97f6c7d 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
@@ -101,6 +101,14 @@
return hostView
}
+ fun deleteAppWidget(hostView: TestAppWidgetHostView) {
+ val appWidgetId = hostView.appWidgetId
+ mHost?.deleteAppWidgetId(appWidgetId)
+ mHostViews.remove(hostView)
+ val contentFrame = findViewById<FrameLayout>(R.id.content)
+ contentFrame.removeView(hostView)
+ }
+
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateAllSizes(newConfig.orientation)
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index bbf99f3..65e7965 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -34,15 +34,21 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.intPreferencesKey
import androidx.glance.Button
+import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.LocalSize
import androidx.glance.action.actionLaunchActivity
+import androidx.glance.appwidget.state.getAppWidgetState
+import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.appwidget.test.R
import androidx.glance.background
+import androidx.glance.currentState
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
@@ -53,6 +59,7 @@
import androidx.glance.layout.height
import androidx.glance.layout.width
import androidx.glance.layout.wrapContentHeight
+import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
@@ -60,10 +67,17 @@
import androidx.glance.text.TextStyle
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
import kotlin.test.assertIs
import kotlin.test.assertNotNull
@@ -73,6 +87,8 @@
@get:Rule
val mHostRule = AppWidgetHostRule()
+ val context = InstrumentationRegistry.getInstrumentation().targetContext!!
+
@Before
fun setUp() {
// Reset the size mode to the default
@@ -432,7 +448,7 @@
@Test
fun bitmapBackground() {
- TestGlanceAppWidget.uiDefinition = compose@{
+ TestGlanceAppWidget.uiDefinition = {
val context = LocalContext.current
val bitmap =
(context.resources.getDrawable(R.drawable.compose, null) as BitmapDrawable).bitmap
@@ -455,8 +471,156 @@
}
}
+ @Test
+ fun removeAppWidget() {
+ TestGlanceAppWidget.stateDefinition = PreferencesGlanceStateDefinition
+ TestGlanceAppWidget.uiDefinition = {
+ Text("something")
+ }
+
+ mHostRule.startHost()
+
+ val appWidgetManager = GlanceAppWidgetManager(context)
+ val glanceId = runBlocking {
+ appWidgetManager.getGlanceIds(TestGlanceAppWidget::class.java).single()
+ }
+
+ runBlocking {
+ TestGlanceAppWidget.updateAppWidgetState<Preferences>(context, glanceId) { prefs ->
+ prefs.toMutablePreferences().apply {
+ this[testKey] = 3
+ }
+ }
+ }
+
+ val fileKey = createUniqueRemoteUiName((glanceId as AppWidgetId).appWidgetId)
+ val preferencesFile = PreferencesGlanceStateDefinition.getLocation(context, fileKey)
+
+ assertThat(preferencesFile.exists())
+
+ val deleteLatch = CountDownLatch(1)
+ TestGlanceAppWidget.setOnDeleteBlock {
+ deleteLatch.countDown()
+ }
+
+ mHostRule.removeAppWidget()
+
+ deleteLatch.await(5, TimeUnit.SECONDS)
+ val interval = 200L
+ for (timeout in 0..2000L step interval) {
+ if (!preferencesFile.exists()) return
+ Thread.sleep(interval)
+ }
+ assertWithMessage("View state file exists").that(preferencesFile.exists())
+ .isFalse()
+ }
+
+ @Test
+ fun updateAll() {
+ TestGlanceAppWidget.uiDefinition = {
+ Text("before")
+ }
+
+ mHostRule.startHost()
+
+ val didRun = AtomicBoolean(false)
+ TestGlanceAppWidget.uiDefinition = {
+ didRun.set(true)
+ Text("after")
+ }
+
+ runBlocking {
+ TestGlanceAppWidget.updateAll(context)
+ }
+ assertThat(didRun.get()).isTrue()
+ }
+
+ @Test
+ fun updateIf() {
+ TestGlanceAppWidget.stateDefinition = PreferencesGlanceStateDefinition
+
+ TestGlanceAppWidget.uiDefinition = {
+ Text("before")
+ }
+
+ mHostRule.startHost()
+
+ val appWidgetManager = GlanceAppWidgetManager(context)
+ runBlocking {
+ appWidgetManager.getGlanceIds(TestGlanceAppWidget::class.java)
+ .forEach { glanceId ->
+ updateAppWidgetState(
+ context,
+ PreferencesGlanceStateDefinition,
+ glanceId
+ ) { prefs ->
+ prefs.toMutablePreferences().apply {
+ this[testKey] = 2
+ }
+ }
+ }
+ }
+
+ // Make sure the app widget is updated if the test is true
+ val didRun = AtomicBoolean(false)
+ TestGlanceAppWidget.uiDefinition = {
+ didRun.set(true)
+ Text("after")
+ }
+ runBlocking {
+ TestGlanceAppWidget.updateIf<Preferences>(context) { prefs ->
+ prefs[testKey] == 2
+ }
+ }
+
+ assertThat(didRun.get()).isTrue()
+
+ // Make sure it is not if the test is false
+ didRun.set(false)
+ runBlocking {
+ TestGlanceAppWidget.updateIf<Preferences>(context) { prefs ->
+ prefs[testKey] == 3
+ }
+ }
+
+ assertThat(didRun.get()).isFalse()
+ }
+
+ @Test
+ fun viewState() {
+ TestGlanceAppWidget.stateDefinition = PreferencesGlanceStateDefinition
+
+ TestGlanceAppWidget.uiDefinition = {
+ val value = currentState<Preferences>()[testKey] ?: -1
+ Text("Value = $value")
+ }
+
+ mHostRule.startHost()
+
+ val appWidgetId = AtomicReference<GlanceId>()
+ mHostRule.onHostView { view ->
+ appWidgetId.set(AppWidgetId(view.appWidgetId))
+ }
+
+ runBlocking {
+ TestGlanceAppWidget.updateAppWidgetState<Preferences>(
+ context,
+ appWidgetId.get()
+ ) { prefs ->
+ prefs.toMutablePreferences().apply {
+ this[testKey] = 2
+ }
+ }
+
+ val prefs =
+ TestGlanceAppWidget.getAppWidgetState<Preferences>(context, appWidgetId.get())
+ assertThat(prefs[testKey]).isEqualTo(2)
+ }
+ }
+
// Check there is a single span of the given type and that it passes the [check].
- private inline fun <reified T> SpannedString.checkHasSingleTypedSpan(check: (T) -> Unit) {
+ private inline
+ fun <reified T> SpannedString.checkHasSingleTypedSpan(check: (T) -> Unit) {
val spans = getSpans(0, length, T::class.java)
assertThat(spans).hasLength(1)
check(spans[0])
@@ -464,8 +628,10 @@
private fun assertViewSize(view: View, expectedSize: DpSize) {
val density = view.context.resources.displayMetrics.density
- assertThat(view.width / density).isWithin(1.1f / density).of(expectedSize.width.value)
- assertThat(view.height / density).isWithin(1.1f / density).of(expectedSize.height.value)
+ assertThat(view.width / density).isWithin(1.1f / density)
+ .of(expectedSize.width.value)
+ assertThat(view.height / density).isWithin(1.1f / density)
+ .of(expectedSize.height.value)
}
private fun assertViewDimension(view: View, sizePx: Int, expectedSize: Dp) {
@@ -473,3 +639,5 @@
assertThat(sizePx / density).isWithin(1.1f / density).of(expectedSize.value)
}
}
+
+private val testKey = intPreferencesKey("testKey")
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
index 5f17899..12ac3f0 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
@@ -17,6 +17,8 @@
package androidx.glance.appwidget
import androidx.compose.runtime.Composable
+import androidx.glance.GlanceId
+import androidx.glance.state.GlanceStateDefinition
class TestGlanceAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = TestGlanceAppWidget
@@ -24,6 +26,8 @@
object TestGlanceAppWidget : GlanceAppWidget(errorUiLayout = 0) {
+ override var stateDefinition: GlanceStateDefinition<*>? = null
+
override var sizeMode: SizeMode = SizeMode.Single
@Composable
@@ -31,5 +35,19 @@
uiDefinition()
}
+ private var onDeleteBlock: ((GlanceId) -> Unit)? = null
+
+ fun setOnDeleteBlock(block: (GlanceId) -> Unit) {
+ onDeleteBlock = block
+ }
+
+ fun resetOnDeleteBlock() {
+ onDeleteBlock = null
+ }
+
+ override suspend fun onDelete(glanceId: GlanceId) {
+ onDeleteBlock?.apply { this(glanceId) }
+ }
+
var uiDefinition: @Composable () -> Unit = { }
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index 56e7a11..05336fd 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -19,7 +19,6 @@
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
-import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
@@ -43,6 +42,7 @@
import androidx.glance.LocalGlanceId
import androidx.glance.LocalSize
import androidx.glance.LocalState
+import androidx.glance.appwidget.state.getAppWidgetState
import kotlinx.coroutines.CancellationException
import androidx.glance.state.GlanceState
import androidx.glance.state.GlanceStateDefinition
@@ -85,6 +85,13 @@
public open val stateDefinition: GlanceStateDefinition<*>? = null
/**
+ * Method called by the framework when an App Widget has been removed from its host.
+ *
+ * When the method returns, the state associated with the [glanceId] will be deleted.
+ */
+ public open suspend fun onDelete(glanceId: GlanceId) { }
+
+ /**
* Triggers the composition of [Content] and sends the result to the [AppWidgetManager].
*/
public suspend fun update(context: Context, glanceId: GlanceId) {
@@ -95,6 +102,26 @@
}
/**
+ * Calls [onDelete], then clear local data associated with the [appWidgetId].
+ *
+ * This is meant to be called when the App Widget instance has been deleted from the host.
+ */
+ internal suspend fun deleted(context: Context, appWidgetId: Int) {
+ val glanceId = AppWidgetId(appWidgetId)
+ try {
+ onDelete(glanceId)
+ } catch (cancelled: CancellationException) {
+ // Nothing to do here
+ } catch (t: Throwable) {
+ Log.e(GlanceAppWidgetTag, "Error in user-provided deletion callback", t)
+ } finally {
+ stateDefinition?.let {
+ GlanceState.deleteStore(context, it, createUniqueRemoteUiName(appWidgetId))
+ }
+ }
+ }
+
+ /**
* Internal version of [update], to be used by the broadcast receiver directly.
*/
internal suspend fun update(
@@ -505,14 +532,24 @@
Log.e(GlanceAppWidgetTag, "Error in Glance App Widget", throwable)
}
-private fun Intent.extractAppWidgetIds() =
- getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
- ?: intArrayOf(
- getIntExtra(
- AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID
- ).also {
- check(it != AppWidgetManager.INVALID_APPWIDGET_ID) {
- "Cannot determine the app widget id"
- }
- })
+/** Update all App Widgets managed by the [GlanceAppWidget] class. */
+public suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
+ val manager = GlanceAppWidgetManager(context)
+ manager.getGlanceIds(javaClass).forEach { update(context, it) }
+}
+
+/**
+ * Update all App Widgets managed by the [GlanceAppWidget] class, if they fulfill some condition.
+ */
+public suspend inline fun <reified State> GlanceAppWidget.updateIf(
+ @Suppress("ContextFirst") context: Context,
+ predicate: (State) -> Boolean
+) {
+ val stateDef = stateDefinition
+ requireNotNull(stateDef) { "GlanceAppWidget.updateIf cannot be used if no state is defined." }
+ val manager = GlanceAppWidgetManager(context)
+ manager.getGlanceIds(javaClass).forEach { glanceId ->
+ val state = getAppWidgetState(context, stateDef, glanceId) as State
+ if (predicate(state)) update(context, glanceId)
+ }
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt
index 6545359..a276ec9 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt
@@ -43,6 +43,10 @@
*
* Note: If you override any of the [AppWidgetProvider] methods, ensure you call their super-class
* implementation.
+ *
+ * Important: if you override any of the methods of this class, you must call the super
+ * implementation, and you must not call [AppWidgetProvider.goAsync], as it will be called by the
+ * super implementation. This means your processing time must be short.
*/
abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {
@@ -88,10 +92,11 @@
}
}
- override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
- // TODO: When a widget is deleted, delete the datastore
- appWidgetIds?.forEach {
- createUniqueRemoteUiName(it)
+ @CallSuper
+ override fun onDeleted(context: Context, appWidgetIds: IntArray) {
+ goAsync {
+ updateManager(context)
+ appWidgetIds.forEach { glanceAppWidget.deleted(context, it) }
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt
index fc8ed71..3c4455d 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt
@@ -19,6 +19,7 @@
import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.createUniqueRemoteUiName
import androidx.glance.state.GlanceState
import androidx.glance.state.GlanceStateDefinition
@@ -56,3 +57,31 @@
updateState,
)
}
+
+/** Get the state of an App Widget. */
+@Suppress("UNCHECKED_CAST")
+public suspend fun <T> GlanceAppWidget.getAppWidgetState(
+ @Suppress("ContextFirst") context: Context,
+ glanceId: GlanceId
+): T =
+ getAppWidgetState(
+ context,
+ checkNotNull(stateDefinition) { "No state defined in this provider" },
+ glanceId
+ ) as T
+
+/** Update the state of an app widget. */
+@Suppress("UNCHECKED_CAST")
+public suspend fun <T> GlanceAppWidget.updateAppWidgetState(
+ @Suppress("ContextFirst") context: Context,
+ glanceId: GlanceId,
+ updateState: suspend (T) -> T,
+): T =
+ updateAppWidgetState(
+ context,
+ checkNotNull(stateDefinition as GlanceStateDefinition<T>) {
+ "No state defined in this provider"
+ },
+ glanceId,
+ updateState,
+ )
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
index 3be6ce2..51b9999 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
@@ -62,43 +62,57 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public object GlanceState {
- // TODO(b/205496180): Make methods internal
/**
* Returns the stored data associated with the given UI key string.
*
* @param definition the configuration that defines this state.
- * @param fileName identifies the data file associated with the store, must be unique for any
+ * @param fileKey identifies the data file associated with the store, must be unique for any
* remote UI in the app.
*/
public suspend fun <T> getValue(
context: Context,
definition: GlanceStateDefinition<T>,
- fileName: String
- ): T = getDataStore(context, definition, fileName).data.first()
+ fileKey: String
+ ): T = getDataStore(context, definition, fileKey).data.first()
/**
* Updates the underlying data by applying the provided update block.
*
* @param definition the configuration that defines this state.
- * @param fileName identifies the date file associated with the store, must be unique for any
+ * @param fileKey identifies the date file associated with the store, must be unique for any
* remote UI in the app.
*/
public suspend fun <T> updateValue(
context: Context,
definition: GlanceStateDefinition<T>,
- fileName: String,
+ fileKey: String,
updateBlock: suspend (T) -> T
- ): T = getDataStore(context, definition, fileName).updateData(updateBlock)
+ ): T = getDataStore(context, definition, fileKey).updateData(updateBlock)
+
+ /**
+ * Delete the file underlying the [DataStore] and remove local references to the [DataStore].
+ */
+ public suspend fun deleteStore(
+ context: Context,
+ definition: GlanceStateDefinition<*>,
+ fileKey: String
+ ) {
+ mutex.withLock {
+ dataStores.remove(fileKey)
+ val location = definition.getLocation(context, fileKey)
+ location.delete()
+ }
+ }
@Suppress("UNCHECKED_CAST")
private suspend fun <T> getDataStore(
context: Context,
definition: GlanceStateDefinition<T>,
- fileName: String
+ fileKey: String
): DataStore<T> =
mutex.withLock {
- dataStores.getOrPut(fileName) {
- definition.getDataStore(context, fileName)
+ dataStores.getOrPut(fileKey) {
+ definition.getDataStore(context, fileKey)
} as DataStore<T>
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index eb02229..ac87caf 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
androidLintMinCompose = "30.0.0"
androidxTestRunner = "1.4.0"
androidxTestRules = "1.4.0"
-androidxTestMonitor = "1.5.0-beta01"
+androidxTestMonitor = "1.5.0-rc01"
androidxTestCore = "1.4.0"
androidxTestExtJunit = "1.1.3"
androidxTestExtTruth = "1.4.0"
@@ -145,7 +145,7 @@
protobufLite = { module = "com.google.protobuf:protobuf-javalite", version = "3.10.0" }
reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.0" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version = "2.7.2" }
-robolectric = { module = "org.robolectric:robolectric", version = "4.6.1" }
+robolectric = { module = "org.robolectric:robolectric", version = "4.7" }
rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version = "2.2.9" }
rxjava3 = { module = "io.reactivex.rxjava3:rxjava", version = "3.0.0" }
shadow = { module = "gradle.plugin.com.github.johnrengelman:shadow", version = "7.1.0" }
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/ActivityNavigatorTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/ActivityNavigatorTest.kt
index c1e144d..fb24f725 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/ActivityNavigatorTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/ActivityNavigatorTest.kt
@@ -302,6 +302,82 @@
}
@Test
+ fun testEquals() {
+ val firstDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ }
+ val secondDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ }
+ assertThat(firstDestination).isEqualTo(secondDestination)
+ }
+
+ @Test
+ fun testFilterEquals() {
+ val firstDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ }
+ val secondDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ assertThat(firstDestination).isEqualTo(secondDestination)
+ }
+
+ @Test
+ fun testEqualsBothIntentNull() {
+ val firstDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ }
+ val secondDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ }
+ assertThat(firstDestination).isEqualTo(secondDestination)
+ }
+
+ @Test
+ fun testNotEquals() {
+ val firstDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ }
+ val secondDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ setAction(TARGET_ACTION)
+ }
+ assertThat(firstDestination).isNotEqualTo(secondDestination)
+ }
+
+ @Test
+ fun testNotEqualsFirstIntentNull() {
+ val firstDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ }
+ val secondDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ }
+ assertThat(firstDestination).isNotEqualTo(secondDestination)
+ }
+
+ @Test
+ fun testNotEqualsSecondIntentNull() {
+ val firstDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ setComponentName(ComponentName(activityRule.activity, TargetActivity::class.java))
+ }
+ val secondDestination = activityNavigator.createDestination().apply {
+ id = TARGET_ID
+ }
+ assertThat(firstDestination).isNotEqualTo(secondDestination)
+ }
+
+ @Test
fun testToString() {
val targetDestination = activityNavigator.createDestination().apply {
id = TARGET_ID
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
index f52061b..8d880eac 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
@@ -407,22 +407,14 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is Destination) return false
return super.equals(other) &&
- intent == other.intent &&
- dataPattern == other.dataPattern &&
- targetPackage == other.targetPackage &&
- component == other.component &&
- action == other.action &&
- data == other.data
+ intent?.filterEquals(other.intent) ?: (other.intent == null) &&
+ dataPattern == other.dataPattern
}
override fun hashCode(): Int {
var result = super.hashCode()
- result = 31 * result + intent.hashCode()
+ result = 31 * result + (intent?.filterHashCode() ?: 0)
result = 31 * result + dataPattern.hashCode()
- result = 31 * result + targetPackage.hashCode()
- result = 31 * result + component.hashCode()
- result = 31 * result + action.hashCode()
- result = 31 * result + data.hashCode()
return result
}
}
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index 47863ba..9a8b93d 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -34,6 +34,7 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
+import android.os.Looper
import android.support.wearable.complications.IPreviewComplicationDataCallback
import android.support.wearable.complications.IProviderInfoService
import android.support.wearable.watchface.Constants
@@ -2175,8 +2176,8 @@
scenario.onActivity { activity ->
val mockWatchFaceHostApi = mock(WatchFaceHostApi::class.java)
- val mockHandler = mock(Handler::class.java)
- `when`(mockWatchFaceHostApi.getUiThreadHandler()).thenReturn(mockHandler)
+ val handler = Handler(Looper.myLooper()!!)
+ `when`(mockWatchFaceHostApi.getUiThreadHandler()).thenReturn(handler)
`when`(mockWatchFaceHostApi.getContext()).thenReturn(
ApplicationProvider.getApplicationContext<Context>()
)
@@ -2222,7 +2223,7 @@
watchState,
mockWatchFaceHostApi,
CompletableDeferred(),
- CoroutineScope(mockHandler.asCoroutineDispatcher())
+ CoroutineScope(handler.asCoroutineDispatcher())
),
null
)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
index 6411050..5e60d2d 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
@@ -184,6 +184,7 @@
context.unregisterReceiver(actionBatteryLowReceiver)
context.unregisterReceiver(actionBatteryOkayReceiver)
context.unregisterReceiver(actionPowerConnectedReceiver)
+ context.unregisterReceiver(actionPowerDisconnectedReceiver)
context.unregisterReceiver(mockTimeReceiver)
}
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 91b47c0..cd03aef 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -671,6 +671,7 @@
if (!watchState.isHeadless) {
WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
+ registerReceivers()
}
val mainScope = CoroutineScope(Dispatchers.Main.immediate)
@@ -806,10 +807,10 @@
@UiThread
private fun registerReceivers() {
+ // Looper can be null in some tests.
require(watchFaceHostApi.getUiThreadHandler().looper.isCurrentThread) {
"registerReceivers must be called the UiThread"
}
-
// There's no point registering BroadcastsReceiver for headless instances.
if (broadcastsReceiver == null && !watchState.isHeadless) {
broadcastsReceiver =
@@ -819,6 +820,7 @@
@UiThread
private fun unregisterReceivers() {
+ // Looper can be null in some tests.
require(watchFaceHostApi.getUiThreadHandler().looper.isCurrentThread) {
"unregisterReceivers must be called the UiThread"
}