Merge "Kotlin 1.6.0" into androidx-main
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index e8c10bf..cc7ef29 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,7 +33,7 @@
because 'Mirror activity dependency graph for -ktx artifacts'
}
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
- api("androidx.savedstate:savedstate-ktx:1.1.0") {
+ api(projectOrArtifact(":savedstate:savedstate-ktx")) {
because 'Mirror activity dependency graph for -ktx artifacts'
}
api(libs.kotlinStdlib)
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 10ce7d3..a2fa9b7 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -20,7 +20,7 @@
api(projectOrArtifact(":core:core"))
api("androidx.lifecycle:lifecycle-runtime:2.3.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
- api("androidx.savedstate:savedstate:1.1.0")
+ api(projectOrArtifact(":savedstate:savedstate"))
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
implementation("androidx.tracing:tracing:1.0.0")
api(libs.kotlinStdlib)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
index 7c1c691..00c2002 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
@@ -18,6 +18,8 @@
import android.os.Bundle
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
import androidx.savedstate.SavedStateRegistry
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -90,6 +92,16 @@
recreate()
}
}
+
+ @Test
+ @Throws(Throwable::class)
+ fun savedStateEarlyRegisterInitAddedLifecycleObserver() {
+ with(ActivityScenario.launch(SavedStateActivity::class.java)) {
+ initializeSavedState()
+ SavedStateActivity.checkEnabledInInitAddedLifecycleObserver = true
+ recreate()
+ }
+ }
}
private class DefaultProvider : SavedStateRegistry.SavedStateProvider {
@@ -115,6 +127,16 @@
checkEnabledInOnContextAvailable = false
}
}
+ lifecycle.addObserver(object : LifecycleEventObserver {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event == Lifecycle.Event.ON_CREATE &&
+ checkEnabledInInitAddedLifecycleObserver) {
+ checkDefaultSavedState(savedStateRegistry)
+ checkEnabledInInitAddedLifecycleObserver = false
+ lifecycle.removeObserver(this)
+ }
+ }
+ })
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -127,6 +149,7 @@
companion object {
internal var checkEnabledInOnCreate = false
+ internal var checkEnabledInInitAddedLifecycleObserver = false
internal var checkEnabledInOnContextAvailable = false
}
}
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index 8990084..030ec7f 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -271,6 +271,7 @@
getLifecycle().removeObserver(this);
}
});
+ mSavedStateRegistryController.performAttach();
if (19 <= SDK_INT && SDK_INT <= 23) {
getLifecycle().addObserver(new ImmLeaksCleaner(this));
diff --git a/busytown/impl/build-studio-and-androidx.sh b/busytown/impl/build-studio-and-androidx.sh
index 2aa37c3..861723a 100755
--- a/busytown/impl/build-studio-and-androidx.sh
+++ b/busytown/impl/build-studio-and-androidx.sh
@@ -52,7 +52,7 @@
function zipStudio() {
cd "$STUDIO_DIR/out/"
- zip -r "$DIST_DIR/tools.zip" repo
+ zip -qr "$DIST_DIR/tools.zip" repo
cd -
}
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 5833337..d16ef32 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -56,6 +56,7 @@
androidTestImplementation(libs.multidex)
androidTestImplementation(project(":camera:camera-lifecycle"))
androidTestImplementation(project(":camera:camera-testing"))
+ androidTestImplementation(project(":camera:camera-video"))
androidTestImplementation(project(":internal-testutils-truth"))
androidTestImplementation(project(":camera:camera-testlib-extensions"))
// To use the testlib to have the implementation of the extensions-stub interface.
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index 81efa20..e883ed5 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -21,6 +21,7 @@
import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
+import androidx.camera.core.VideoCapture
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.extensions.internal.ExtensionVersion
import androidx.camera.extensions.internal.Version
@@ -430,6 +431,23 @@
}
}
+ @Test
+ fun throwIllegalArgumentException_whenBindingVideoCapture(): Unit = runBlocking {
+ val extensionCameraSelector = checkExtensionAvailabilityAndInit()
+
+ withContext(Dispatchers.Main) {
+ val fakeLifecycleOwner = FakeLifecycleOwner()
+
+ assertThrows<IllegalArgumentException> {
+ cameraProvider.bindToLifecycle(
+ fakeLifecycleOwner,
+ extensionCameraSelector,
+ VideoCapture.Builder().build()
+ )
+ }
+ }
+ }
+
private fun checkExtensionAvailabilityAndInit(): CameraSelector {
extensionsManager = ExtensionsManager.getInstanceAsync(
context,
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
index b792fe7..fc383e5 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionsUseCaseConfigFactory.java
@@ -62,6 +62,9 @@
mutableOptionsBundle =
MutableOptionsBundle.from(mPreviewConfigProvider.getConfig());
break;
+ case VIDEO_CAPTURE:
+ throw new IllegalArgumentException("CameraX Extensions doesn't support "
+ + "VideoCapture!");
default:
return null;
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
index a9af102..25f75e6 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
@@ -132,6 +132,7 @@
cameraProvider.shutdown().get(10, TimeUnit.SECONDS);
}
+
@Test
public void testFlashToggleButton() {
waitFor(new WaitForViewToShow(R.id.constraintLayout));
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
index edc57c1..5e03fe3 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
@Composable
fun InputFieldDemo() {
@@ -74,6 +75,13 @@
}
}
+@Composable
+fun DialogInputFieldDemo(onNavigateUp: () -> Unit) {
+ Dialog(onDismissRequest = onNavigateUp) {
+ InputFieldDemo()
+ }
+}
+
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun EditLine(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index db87997..77be109 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -42,7 +42,10 @@
ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
ComposableDemo("Capitalization/AutoCorrect") { CapitalizationAutoCorrectDemo() },
- ComposableDemo("TextFieldValue") { TextFieldValueDemo() }
+ ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
+ ComposableDemo("Inside Dialog") { onNavigateUp ->
+ DialogInputFieldDemo(onNavigateUp)
+ },
)
),
ComposableDemo("Text Accessibility") { TextAccessibilityDemo() }
diff --git a/compose/integration-tests/demos/common/src/main/java/androidx/compose/integration/demos/common/Demo.kt b/compose/integration-tests/demos/common/src/main/java/androidx/compose/integration/demos/common/Demo.kt
index 2c1d864..7271573 100644
--- a/compose/integration-tests/demos/common/src/main/java/androidx/compose/integration/demos/common/Demo.kt
+++ b/compose/integration-tests/demos/common/src/main/java/androidx/compose/integration/demos/common/Demo.kt
@@ -45,7 +45,8 @@
/**
* Demo that displays [Composable] [content] when selected.
*/
-class ComposableDemo(title: String, val content: @Composable () -> Unit) : Demo(title)
+class ComposableDemo(title: String, val content: @Composable (onNavigateUp: () -> Unit) -> Unit) :
+ Demo(title)
/**
* A category of [Demo]s, that will display a list of [demos] when selected.
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
index a878e475..858fa8d 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
@@ -92,7 +92,7 @@
}
) { innerPadding ->
val modifier = Modifier.padding(innerPadding)
- DemoContent(modifier, currentDemo, isFiltering, filterText, onNavigateToDemo)
+ DemoContent(modifier, currentDemo, isFiltering, filterText, onNavigateToDemo, onNavigateUp)
}
}
@@ -102,7 +102,8 @@
currentDemo: Demo,
isFiltering: Boolean,
filterText: String,
- onNavigate: (Demo) -> Unit
+ onNavigate: (Demo) -> Unit,
+ onNavigateUp: () -> Unit
) {
Crossfade(isFiltering to currentDemo) { (filtering, demo) ->
Surface(modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
@@ -113,19 +114,19 @@
onNavigate = onNavigate
)
} else {
- DisplayDemo(demo, onNavigate)
+ DisplayDemo(demo, onNavigate, onNavigateUp)
}
}
}
}
@Composable
-private fun DisplayDemo(demo: Demo, onNavigate: (Demo) -> Unit) {
+private fun DisplayDemo(demo: Demo, onNavigate: (Demo) -> Unit, onNavigateUp: () -> Unit) {
when (demo) {
is ActivityDemo<*> -> {
/* should never get here as activity demos are not added to the backstack*/
}
- is ComposableDemo -> demo.content()
+ is ComposableDemo -> demo.content(onNavigateUp)
is DemoCategory -> DisplayDemoCategory(demo, onNavigate)
is FragmentDemo<*> -> {
lateinit var view: FragmentContainerView
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt
index 13ba40b..cc5e31f 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt
@@ -48,8 +48,6 @@
companion object {
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters() = createStartupCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createStartupCompilationParams()
}
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt
index 4b3fe6b..9b20220 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt
@@ -84,8 +84,6 @@
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun parameters() = createCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createCompilationParams()
}
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 3100c8e..cb9e113 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -49,8 +49,6 @@
companion object {
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters() = createStartupCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createStartupCompilationParams()
}
}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
index 102d4ee..69dc8d6 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
@@ -84,8 +84,6 @@
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun parameters() = createCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createCompilationParams()
}
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
index 94f358c..1a6254b 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -48,8 +48,6 @@
companion object {
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters() = createStartupCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createStartupCompilationParams()
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/RadioButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/RadioButton.kt
index 22cb755..ebc9347 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/RadioButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/RadioButton.kt
@@ -91,7 +91,7 @@
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
- radius = RadioButtonRippleRadius
+ radius = RadioButtonTokens.StateLayerSize / 2
)
)
} else {
@@ -240,7 +240,6 @@
private const val RadioAnimationDuration = 100
-private val RadioButtonRippleRadius = 24.dp
private val RadioButtonPadding = 2.dp
private val RadioButtonDotSize = 12.dp
private val RadioStrokeWidth = 2.dp
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt
new file mode 100644
index 0000000..1183a84
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupLayoutTest.kt
@@ -0,0 +1,302 @@
+/*
+ * 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.ui.window
+
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.UUID
+
+/**
+ * Tests for the internal [PopupLayout] view used by [Popup].
+ * When adding new tests, consider writing the tests against the [Popup] composable directly first,
+ * since that's the public API, and only adding tests here if the tests need to interact in ways
+ * that aren't easily supported by the compose test APIs.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PopupLayoutTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun canCalculatePosition_onlyWhenSizeAndCoordinatesAreAvailable() {
+ val layout = createPopupLayout()
+ assertThat(layout.canCalculatePosition).isFalse()
+
+ // Only size available.
+ layout.popupContentSize = IntSize.Zero
+ assertThat(layout.canCalculatePosition).isFalse()
+
+ // Only coordinates available.
+ layout.popupContentSize = null
+ layout.updateParentLayoutCoordinates(NoopLayoutCoordinates)
+ assertThat(layout.canCalculatePosition).isFalse()
+
+ // Everything available.
+ layout.popupContentSize = IntSize.Zero
+ assertThat(layout.canCalculatePosition).isTrue()
+ }
+
+ @Test
+ fun positionUpdated_whenCoordinatesUpdated() {
+ val coordinates = MutableLayoutCoordinates()
+ val layout = createPopupLayout(
+ positionProvider = object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset = anchorBounds.topLeft
+ },
+ )
+ layout.popupContentSize = IntSize.Zero
+
+ assertThat(layout.params.x).isEqualTo(0)
+ assertThat(layout.params.y).isEqualTo(0)
+
+ coordinates.windowOffset = Offset(50f, 50f)
+ layout.updateParentLayoutCoordinates(coordinates)
+
+ assertThat(layout.params.x).isEqualTo(50)
+ assertThat(layout.params.y).isEqualTo(50)
+ }
+
+ @Test
+ fun positionNotUpdated_whenCoordinatesUpdated_withSameParentBounds() {
+ var paramUpdateCount = 0
+ val layout = createPopupLayout(
+ positionProvider = object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset = anchorBounds.topLeft
+ },
+ popupLayoutHelper = object : NoopPopupLayoutHelper() {
+ override fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ ) {
+ paramUpdateCount++
+ }
+ }
+ )
+
+ // Set size before coordinates to match the order that the compose runtime uses.
+ layout.popupContentSize = IntSize.Zero
+ layout.updateParentLayoutCoordinates(MutableLayoutCoordinates())
+
+ assertThat(paramUpdateCount).isEqualTo(1)
+
+ // Different coordinates object but with the same values, so shouldn't trigger a position
+ // update.
+ layout.updateParentLayoutCoordinates(MutableLayoutCoordinates())
+
+ assertThat(paramUpdateCount).isEqualTo(1)
+ }
+
+ @Test
+ fun positionNotUpdated_onParentBoundsUpdateRequested_withSameParentBounds() {
+ var paramUpdateCount = 0
+ val layout = createPopupLayout(
+ positionProvider = object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset = anchorBounds.topLeft
+ },
+ popupLayoutHelper = object : NoopPopupLayoutHelper() {
+ override fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ ) {
+ paramUpdateCount++
+ }
+ }
+ )
+
+ // Set size before coordinates to match the order that the compose runtime uses.
+ layout.popupContentSize = IntSize.Zero
+ layout.updateParentLayoutCoordinates(MutableLayoutCoordinates())
+
+ assertThat(paramUpdateCount).isEqualTo(1)
+
+ layout.updateParentBounds()
+
+ assertThat(paramUpdateCount).isEqualTo(1)
+ }
+
+ @Test
+ fun positionUpdated_onParentBoundsUpdateRequested_withDifferentParentBounds() {
+ var paramUpdateCount = 0
+ val coordinates = MutableLayoutCoordinates()
+ val layout = createPopupLayout(
+ positionProvider = object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset = anchorBounds.topLeft
+ },
+ popupLayoutHelper = object : NoopPopupLayoutHelper() {
+ override fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ ) {
+ paramUpdateCount++
+ }
+ }
+ )
+
+ // Set size before coordinates to match the order that the compose runtime uses.
+ layout.popupContentSize = IntSize.Zero
+ layout.updateParentLayoutCoordinates(coordinates)
+
+ assertThat(layout.params.x).isEqualTo(0)
+ assertThat(layout.params.y).isEqualTo(0)
+
+ coordinates.windowOffset = Offset(50f, 50f)
+ layout.updateParentBounds()
+
+ assertThat(layout.params.x).isEqualTo(50)
+ assertThat(layout.params.y).isEqualTo(50)
+ }
+
+ private fun createPopupLayout(
+ onDismissRequest: (() -> Unit)? = null,
+ properties: PopupProperties = PopupProperties(),
+ density: Density = rule.density,
+ positionProvider: PopupPositionProvider = ZeroPositionProvider,
+ popupLayoutHelper: PopupLayoutHelper = NoopPopupLayoutHelper()
+ ): PopupLayout {
+ lateinit var layout: PopupLayout
+ rule.setContent {
+ val view = LocalView.current
+ remember {
+ PopupLayout(
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ testTag = "test popup",
+ composeView = view,
+ density = density,
+ initialPositionProvider = positionProvider,
+ popupId = UUID.randomUUID(),
+ popupLayoutHelper = popupLayoutHelper
+ ).also { layout = it }
+ }
+ }
+ return rule.runOnIdle { layout }
+ }
+
+ private companion object {
+ val ZeroPositionProvider = object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset = IntOffset.Zero
+ }
+
+ val NoopLayoutCoordinates: LayoutCoordinates = MutableLayoutCoordinates()
+
+ /**
+ * An implementation of [LayoutCoordinates] that allows explicitly setting values but only
+ * supports the minimum required subset of operations that [PopupLayout] uses.
+ */
+ private class MutableLayoutCoordinates : LayoutCoordinates {
+ override var size: IntSize = IntSize.Zero
+ override val providedAlignmentLines: Set<AlignmentLine> = emptySet()
+ override var parentLayoutCoordinates: LayoutCoordinates? = null
+ override var parentCoordinates: LayoutCoordinates? = null
+ override var isAttached: Boolean = false
+
+ var windowOffset: Offset = Offset.Zero
+
+ override fun windowToLocal(relativeToWindow: Offset): Offset =
+ relativeToWindow - windowOffset
+
+ override fun localToWindow(relativeToLocal: Offset): Offset =
+ windowOffset + relativeToLocal
+
+ override fun localToRoot(relativeToLocal: Offset): Offset =
+ throw UnsupportedOperationException()
+
+ override fun localPositionOf(
+ sourceCoordinates: LayoutCoordinates,
+ relativeToSource: Offset
+ ): Offset = throw UnsupportedOperationException()
+
+ override fun localBoundingBoxOf(
+ sourceCoordinates: LayoutCoordinates,
+ clipBounds: Boolean
+ ): Rect = throw UnsupportedOperationException()
+
+ override fun get(alignmentLine: AlignmentLine): Int =
+ throw UnsupportedOperationException()
+ }
+
+ private open class NoopPopupLayoutHelper : PopupLayoutHelper {
+ override fun getWindowVisibleDisplayFrame(
+ composeView: View,
+ outRect: android.graphics.Rect
+ ) {
+ // do nothing
+ }
+
+ override fun setGestureExclusionRects(composeView: View, width: Int, height: Int) {
+ // do nothing
+ }
+
+ override fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ ) {
+ // do nothing
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 0fe17b6..60fbbcc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -27,15 +27,17 @@
import android.view.MotionEvent
import android.view.View
import android.view.View.MeasureSpec.makeMeasureSpec
+import android.view.ViewGroup
import android.view.ViewOutlineProvider
-import android.view.ViewTreeObserver
import android.view.WindowManager
import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
@@ -52,6 +54,7 @@
import androidx.compose.ui.R
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
@@ -72,6 +75,8 @@
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.ViewTreeViewModelStoreOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
+import kotlinx.coroutines.android.awaitFrame
+import kotlinx.coroutines.isActive
import org.jetbrains.annotations.TestOnly
import java.util.UUID
import kotlin.math.roundToInt
@@ -281,22 +286,33 @@
onDispose {}
}
+ // The parent's bounds can change on any frame without onGloballyPositioned being called, if
+ // e.g. the soft keyboard changes visibility. For that reason, we need to check if we've moved
+ // on every frame. However, we don't need to handle all moves – most position changes will be
+ // handled by onGloballyPositioned. This polling loop only needs to handle the case where the
+ // view's absolute position on the screen has changed, so we do a quick check to see if it has,
+ // and only do the other position calculations in that case.
+ LaunchedEffect(popupLayout) {
+ while (isActive) {
+ awaitFrame()
+ popupLayout.pollForLocationOnScreenChange()
+ }
+ }
+
// TODO(soboleva): Look at module arrangement so that Box can be
// used instead of this custom Layout
// Get the parent's position, size and layout direction
Layout(
content = {},
- modifier = Modifier.onGloballyPositioned { childCoordinates ->
- val coordinates = childCoordinates.parentLayoutCoordinates!!
- val layoutSize = coordinates.size
-
- val position = coordinates.positionInWindow()
- val layoutPosition = IntOffset(position.x.roundToInt(), position.y.roundToInt())
-
- popupLayout.parentBounds = IntRect(layoutPosition, layoutSize)
- // Update the popup's position
- popupLayout.updatePosition()
- }
+ modifier = Modifier
+ .onGloballyPositioned { childCoordinates ->
+ // This callback is best-effort – the screen coordinates of this layout node can
+ // change at any time without this callback being fired (e.g. during IME visibility
+ // change). For that reason, updating the position in this callback is not
+ // sufficient, and the coordinates are also re-calculated on every frame.
+ val parentCoordinates = childCoordinates.parentLayoutCoordinates!!
+ popupLayout.updateParentLayoutCoordinates(parentCoordinates)
+ }
) { _, _ ->
popupLayout.parentLayoutDirection = layoutDirection
layout(0, 0) {}
@@ -353,46 +369,45 @@
* @param composeView The parent view of the popup which is the AndroidComposeView.
*/
@SuppressLint("ViewConstructor")
-private class PopupLayout(
+internal class PopupLayout(
private var onDismissRequest: (() -> Unit)?,
private var properties: PopupProperties,
var testTag: String,
private val composeView: View,
density: Density,
initialPositionProvider: PopupPositionProvider,
- popupId: UUID
+ popupId: UUID,
+ private val popupLayoutHelper: PopupLayoutHelper = if (Build.VERSION.SDK_INT >= 29) {
+ PopupLayoutHelperImpl29()
+ } else {
+ PopupLayoutHelperImpl()
+ }
) : AbstractComposeView(composeView.context),
- ViewRootForInspector,
- ViewTreeObserver.OnGlobalLayoutListener,
- View.OnAttachStateChangeListener {
+ ViewRootForInspector {
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- private val params = createLayoutParams()
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val params = createLayoutParams()
/** The logic of positioning the popup relative to its parent. */
var positionProvider = initialPositionProvider
// Position params
var parentLayoutDirection: LayoutDirection = LayoutDirection.Ltr
- var parentBounds: IntRect? by mutableStateOf(null)
var popupContentSize: IntSize? by mutableStateOf(null)
+ private var parentLayoutCoordinates: LayoutCoordinates? by mutableStateOf(null)
+ private var parentBounds: IntRect? = null
- // Track parent bounds and content size; only show popup once we have both
- val canCalculatePosition by derivedStateOf { parentBounds != null && popupContentSize != null }
+ /** Track parent coordinates and content size; only show popup once we have both. */
+ val canCalculatePosition by derivedStateOf {
+ parentLayoutCoordinates != null && popupContentSize != null
+ }
private val maxSupportedElevation = 30.dp
- private val popupLayoutHelper: PopupLayoutHelper = if (Build.VERSION.SDK_INT >= 29) {
- PopupLayoutHelperImpl29()
- } else {
- PopupLayoutHelperImpl()
- }
-
// The window visible frame used for the last popup position calculation.
private val previousWindowVisibleFrame = Rect()
- private val tmpWindowVisibleFrame = Rect()
- // Whether the PopupLayout is currently listening to global layout changes.
- private var isListeningToGlobalLayout = false
override val subCompositionView: AbstractComposeView get() = this
@@ -401,12 +416,6 @@
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))
- // We are listening to composeView detach changes, in order to remove properly the
- // global layout listener we add to its viewTreeObserver. We cannot just remove the
- // listener in during popup's dismiss as this can happen only after the composeView was
- // detached and no longer references the original viewTreeObserver.
- composeView.addOnAttachStateChangeListener(this)
- registerOnGlobalLayoutListener()
// Set unique id for AbstractComposeView. This allows state restoration for the state
// defined inside the Popup via rememberSaveable()
setTag(R.id.compose_view_saveable_id_tag, "Popup:$popupId")
@@ -470,7 +479,7 @@
val child = getChildAt(0) ?: return
params.width = child.measuredWidth
params.height = child.measuredHeight
- windowManager.updateViewLayout(this, params)
+ popupLayoutHelper.updateViewLayout(windowManager, this, params)
}
private val displayWidth: Int
@@ -556,7 +565,63 @@
private fun applyNewFlags(flags: Int) {
params.flags = flags
- windowManager.updateViewLayout(this, params)
+ popupLayoutHelper.updateViewLayout(windowManager, this, params)
+ }
+
+ /**
+ * Updates the [LayoutCoordinates] object that is used by [updateParentBounds] to calculate
+ * the position of the popup. If the new [LayoutCoordinates] reports new parent bounds, calls
+ * [updatePosition].
+ */
+ fun updateParentLayoutCoordinates(parentLayoutCoordinates: LayoutCoordinates) {
+ this.parentLayoutCoordinates = parentLayoutCoordinates
+ updateParentBounds()
+ }
+
+ /**
+ * Used by [pollForLocationOnScreenChange] to read the [composeView]'s absolute position
+ * on screen. The array is stored as a field instead of allocated in the method because it's
+ * called on every frame.
+ */
+ private val locationOnScreen = IntArray(2)
+
+ /**
+ * Returns true if the absolute location of the [composeView] on the screen has changed since
+ * the last call. This method asks the view for its location instead of using Compose APIs like
+ * [LayoutCoordinates] because it does less work, and this method is intended to be called on
+ * every frame.
+ *
+ * The location can change without any callbacks being fired if, for example, the soft keyboard
+ * is shown or hidden when the window is in `adjustPan` mode. In that case, the window's root
+ * view (`ViewRootImpl`) will "scroll" the view hierarchy in a special way that doesn't fire any
+ * callbacks.
+ */
+ fun pollForLocationOnScreenChange() {
+ val (oldX, oldY) = locationOnScreen
+ composeView.getLocationOnScreen(locationOnScreen)
+ if (oldX != locationOnScreen[0] || oldY != locationOnScreen[1]) {
+ updateParentBounds()
+ }
+ }
+
+ /**
+ * Re-calculates the bounds of the parent layout node that this popup is anchored to. If they've
+ * changed since the last call, calls [updatePosition] to actually calculate the popup's new
+ * position and update the window.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun updateParentBounds() {
+ val coordinates = parentLayoutCoordinates ?: return
+ val layoutSize = coordinates.size
+
+ val position = coordinates.positionInWindow()
+ val layoutPosition = IntOffset(position.x.roundToInt(), position.y.roundToInt())
+
+ val newParentBounds = IntRect(layoutPosition, layoutSize)
+ if (newParentBounds != parentBounds) {
+ this.parentBounds = newParentBounds
+ updatePosition()
+ }
}
/**
@@ -567,7 +632,7 @@
val popupContentSize = popupContentSize ?: return
val windowSize = previousWindowVisibleFrame.let {
- composeView.getWindowVisibleDisplayFrame(it)
+ popupLayoutHelper.getWindowVisibleDisplayFrame(composeView, it)
val bounds = it.toIntBounds()
IntSize(width = bounds.width, height = bounds.height)
}
@@ -588,7 +653,7 @@
popupLayoutHelper.setGestureExclusionRects(this, windowSize.width, windowSize.height)
}
- windowManager.updateViewLayout(this, params)
+ popupLayoutHelper.updateViewLayout(windowManager, this, params)
}
/**
@@ -596,8 +661,6 @@
*/
fun dismiss() {
ViewTreeLifecycleOwner.set(this, null)
- unregisterOnGlobalLayoutListener()
- composeView.removeOnAttachStateChangeListener(this)
windowManager.removeViewImmediate(this)
}
@@ -660,7 +723,7 @@
// Enables us to intercept outside clicks even when popup is not focusable
flags = flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL
// Get the Window token from the parent view
token = composeView.applicationWindowToken
@@ -676,51 +739,43 @@
title = composeView.context.resources.getString(R.string.default_popup_window_title)
}
}
-
- private fun Rect.toIntBounds() = IntRect(
- left = left,
- top = top,
- right = right,
- bottom = bottom
- )
-
- override fun onViewAttachedToWindow(composeView: View) = registerOnGlobalLayoutListener()
-
- override fun onViewDetachedFromWindow(composeView: View) = unregisterOnGlobalLayoutListener()
-
- private fun registerOnGlobalLayoutListener() {
- if (isListeningToGlobalLayout || !composeView.isAttachedToWindow) return
- composeView.viewTreeObserver.addOnGlobalLayoutListener(this)
- isListeningToGlobalLayout = true
- }
-
- private fun unregisterOnGlobalLayoutListener() {
- if (!isListeningToGlobalLayout) return
- composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
- isListeningToGlobalLayout = false
- }
-
- override fun onGlobalLayout() {
- // Update the position of the popup, in case getWindowVisibleDisplayFrame has changed.
- composeView.getWindowVisibleDisplayFrame(tmpWindowVisibleFrame)
- if (tmpWindowVisibleFrame != previousWindowVisibleFrame) {
- updatePosition()
- }
- }
}
-private interface PopupLayoutHelper {
+/**
+ * Collection of methods delegated to platform methods to support APIs only available on newer
+ * platforms and testing.
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+internal interface PopupLayoutHelper {
+ fun getWindowVisibleDisplayFrame(composeView: View, outRect: Rect)
fun setGestureExclusionRects(composeView: View, width: Int, height: Int)
+ fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ )
}
-private class PopupLayoutHelperImpl : PopupLayoutHelper {
+private open class PopupLayoutHelperImpl : PopupLayoutHelper {
+ override fun getWindowVisibleDisplayFrame(composeView: View, outRect: Rect) {
+ composeView.getWindowVisibleDisplayFrame(outRect)
+ }
+
override fun setGestureExclusionRects(composeView: View, width: Int, height: Int) {
// do nothing
}
+
+ override fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ ) {
+ windowManager.updateViewLayout(popupView, params)
+ }
}
@RequiresApi(29)
-private class PopupLayoutHelperImpl29 : PopupLayoutHelper {
+private class PopupLayoutHelperImpl29 : PopupLayoutHelperImpl() {
override fun setGestureExclusionRects(composeView: View, width: Int, height: Int) {
composeView.systemGestureExclusionRects = mutableListOf(
Rect(
@@ -741,6 +796,13 @@
return false
}
+private fun Rect.toIntBounds() = IntRect(
+ left = left,
+ top = top,
+ right = right,
+ bottom = bottom
+)
+
/**
* Returns whether the given view is an underlying decor view of a popup. If the given testTag is
* supplied it also verifies that the popup has such tag assigned.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt
index c636e8c..7fde46e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt
@@ -27,6 +27,13 @@
* global position of the content may have changed.
* Note that it will be called **after** a composition when the coordinates are finalized.
*
+ * This callback will be invoked at least once when the [LayoutCoordinates] are available, and every
+ * time the element's position changes within the window. However, it is not guaranteed to be
+ * invoked every time the position _relative to the screen_ of the modified element changes. For
+ * example, the system may move the contents inside a window around without firing a callback.
+ * If you are using the [LayoutCoordinates] to calculate position on the screen, and not just inside
+ * the window, you may not receive a callback.
+ *
* Usage example:
* @sample androidx.compose.ui.samples.OnGloballyPositioned
*/
diff --git a/core/core-performance/build.gradle b/core/core-performance/build.gradle
index 8b5f982..67c0305 100644
--- a/core/core-performance/build.gradle
+++ b/core/core-performance/build.gradle
@@ -26,7 +26,9 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
}
androidx {
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
new file mode 100644
index 0000000..a3a849c
--- /dev/null
+++ b/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.core.performance
+
+/**
+ * Reports the media performance class of the device.
+ */
+class PerformanceClass {
+
+ /**
+ * The media performance class of the device or 0 if none.
+ * <p>
+ * If this value is not <code>0</code>, the device conforms to the media performance class
+ * definition of the SDK version of this value. This value never changes while a device is
+ * booted, but it may increase when the hardware manufacturer provides an OTA update.
+ * <p>
+ * Possible non-zero values are defined in {@link Build.VERSION_CODES} starting with
+ * {@link Build.VERSION_CODES#R}.
+ * <p>
+ * Defaults to {@link Build.MEDIA_PERFORMANCE_CLASS}
+ */
+ fun getPerformanceClass(): Int {
+ return 0
+ }
+}
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
new file mode 100644
index 0000000..294d3e9
--- /dev/null
+++ b/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.core.performance
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** Unit tests for [PerformanceClass]. */
+@RunWith(JUnit4::class)
+class PerformanceClassTest {
+
+ private val pc = PerformanceClass()
+
+ @Test
+ fun getPerformanceClass() {
+ assertThat(pc.getPerformanceClass()).isEqualTo(0)
+ }
+}
\ No newline at end of file
diff --git a/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
index b27dd32..3c5ea44 100644
--- a/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
@@ -465,6 +465,7 @@
assertMockAccessibilityDelegateWorkingOnView(mockDelegate);
}
+ @FlakyTest(bugId = 206644987)
@Test
@SdkSuppress(minSdkVersion = 19)
public void testSetAccessibilityPaneTitle_sendsOutCorrectEvent() throws TimeoutException {
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java
index 77aca19..9c1aa32 100644
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java
@@ -46,6 +46,7 @@
import androidx.lifecycle.testapp.NavigationDialogActivity;
import androidx.lifecycle.testapp.TestEvent;
import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -126,6 +127,7 @@
runTest(activity);
}
+ @FlakyTest(bugId = 206645367)
@Test
public void coveredWithDialog_fragment() throws Throwable {
CollectingSupportFragment fragment = new CollectingSupportFragment();
diff --git a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCallbackTest.java b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCallbackTest.java
index 8f859b1..8a7aaf3 100644
--- a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCallbackTest.java
+++ b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCallbackTest.java
@@ -62,6 +62,7 @@
import androidx.media2.test.client.RemoteMediaSession;
import androidx.media2.test.common.TestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import org.junit.After;
@@ -79,6 +80,7 @@
/**
* Tests {@link MediaController.ControllerCallback}.
*/
+@FlakyTest(bugId = 202942942)
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MediaControllerCallbackTest extends MediaSessionTestBase {
diff --git a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCompatCallbackWithMediaSessionTest.java b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCompatCallbackWithMediaSessionTest.java
index 30bad23..c86c41e 100644
--- a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCompatCallbackWithMediaSessionTest.java
+++ b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerCompatCallbackWithMediaSessionTest.java
@@ -64,6 +64,7 @@
/**
* Tests for {@link MediaControllerCompat.Callback} with {@link MediaSession}.
*/
+@FlakyTest(bugId = 202942942)
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MediaControllerCompatCallbackWithMediaSessionTest extends MediaSessionTestBase {
diff --git a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerTest.java b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerTest.java
index 845c0e3..7002170 100644
--- a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerTest.java
+++ b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaControllerTest.java
@@ -60,6 +60,7 @@
import androidx.media2.test.common.PollingCheck;
import androidx.media2.test.common.TestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import org.junit.After;
@@ -77,6 +78,7 @@
/**
* Tests {@link MediaController}.
*/
+@FlakyTest(bugId = 202942942)
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MediaControllerTest extends MediaSessionTestBase {
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java
index 236caaa..d388a0b 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionCallbackWithMediaControllerCompatTest.java
@@ -63,6 +63,7 @@
import androidx.media2.test.service.MockRemotePlayer;
import androidx.media2.test.service.RemoteMediaControllerCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import org.junit.After;
@@ -78,6 +79,7 @@
/**
* Tests {@link SessionCallback} working with {@link MediaControllerCompat}.
*/
+@FlakyTest(bugId = 202942942)
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MediaSessionCallbackWithMediaControllerCompatTest extends MediaSessionTestBase {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
index 9ad903b..d24da07 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
@@ -149,6 +149,17 @@
}
/**
+ * Returns the [Annotation]s that are annotated with [annotationName]
+ */
+ fun getAnnotationsAnnotatedWith(
+ annotationName: ClassName
+ ): Set<XAnnotation> {
+ return getAllAnnotations().filter {
+ it.type.typeElement?.hasAnnotation(annotationName) ?: false
+ }.toSet()
+ }
+
+ /**
* Returns the [XAnnotation] that has the same qualified name as [annotationName].
*
* @see [hasAnnotation]
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
index de97611..5503384 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
@@ -119,6 +119,63 @@
}
@Test
+ fun getAnnotationsAnnotatedWith() {
+ val source = Source.kotlin(
+ "MyClass.kt",
+ """
+ package foo.bar
+
+ @Target(AnnotationTarget.ANNOTATION_CLASS)
+ @Retention(AnnotationRetention.SOURCE)
+ annotation class SourceAnnotation
+
+ @Target(AnnotationTarget.ANNOTATION_CLASS)
+ @Retention(AnnotationRetention.BINARY)
+ annotation class BinaryAnnotation
+
+ @Target(AnnotationTarget.ANNOTATION_CLASS)
+ @Retention(AnnotationRetention.RUNTIME)
+ annotation class RuntimeAnnotation
+
+ @SourceAnnotation
+ @BinaryAnnotation
+ @RuntimeAnnotation
+ @Target(AnnotationTarget.CLASS)
+ @Retention(AnnotationRetention.RUNTIME)
+ annotation class Foo
+
+ @Foo
+ class MyClass
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(source),
+ ) { invocation ->
+ val element = invocation.processingEnv.requireTypeElement("foo.bar.MyClass")
+
+ val annotationsForAnnotations = if (preCompiled) {
+ // Source level annotations are gone if it's pre-compiled
+ listOf("BinaryAnnotation", "RuntimeAnnotation")
+ } else {
+ listOf("SourceAnnotation", "BinaryAnnotation", "RuntimeAnnotation")
+ }
+
+ annotationsForAnnotations.forEach {
+ val annotations = element.getAnnotationsAnnotatedWith(
+ ClassName.get("foo.bar", it))
+ assertThat(annotations).hasSize(1)
+ val annotation = annotations.first()
+ assertThat(annotation.name)
+ .isEqualTo("Foo")
+ assertThat(annotation.qualifiedName)
+ .isEqualTo("foo.bar.Foo")
+ assertThat(annotation.type.typeElement)
+ .isEqualTo(invocation.processingEnv.requireTypeElement("foo.bar.Foo"))
+ }
+ }
+ }
+
+ @Test
fun annotationsInClassPathCanBeBoxed() {
val source = Source.kotlin(
"MyClass.kt",
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index aac862a..71beea9 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -20,6 +20,7 @@
public final class SavedStateRegistryController {
method public static androidx.savedstate.SavedStateRegistryController create(androidx.savedstate.SavedStateRegistryOwner);
method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+ method @MainThread public void performAttach();
method @MainThread public void performRestore(android.os.Bundle?);
method @MainThread public void performSave(android.os.Bundle);
}
diff --git a/savedstate/savedstate/api/public_plus_experimental_current.txt b/savedstate/savedstate/api/public_plus_experimental_current.txt
index aac862a..71beea9 100644
--- a/savedstate/savedstate/api/public_plus_experimental_current.txt
+++ b/savedstate/savedstate/api/public_plus_experimental_current.txt
@@ -20,6 +20,7 @@
public final class SavedStateRegistryController {
method public static androidx.savedstate.SavedStateRegistryController create(androidx.savedstate.SavedStateRegistryOwner);
method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+ method @MainThread public void performAttach();
method @MainThread public void performRestore(android.os.Bundle?);
method @MainThread public void performSave(android.os.Bundle);
}
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index aac862a..71beea9 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -20,6 +20,7 @@
public final class SavedStateRegistryController {
method public static androidx.savedstate.SavedStateRegistryController create(androidx.savedstate.SavedStateRegistryOwner);
method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
+ method @MainThread public void performAttach();
method @MainThread public void performRestore(android.os.Bundle?);
method @MainThread public void performSave(android.os.Bundle);
}
diff --git a/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt b/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt
index 368ee9a..702bb98 100644
--- a/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt
+++ b/savedstate/savedstate/src/androidTest/java/androidx/savedstate/SavedStateRegistryTest.kt
@@ -18,6 +18,7 @@
import android.os.Bundle
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleRegistry
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -169,6 +170,26 @@
owner.savedStateRegistry.runOnNextRecreation(ToBeRecreated::class.java)
}
+ @UiThreadTest
+ @Test
+ fun runOnNextRecreationFromEarlyRegisteredObserver() {
+ val owner = FakeSavedStateRegistryOwner()
+ owner.savedStateRegistryController.performAttach()
+ // shouldn't throw, though we aren't even created
+ owner.lifecycle.addObserver(
+ LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_START)
+ owner.savedStateRegistry.runOnNextRecreation(ToBeRecreated::class.java)
+ }
+ )
+ owner.savedStateRegistryController.performRestore(null)
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ // now ON_STOP event will be sent
+ owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ // now ON_START event will be sent again, previously registered observer shouldn't throw
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ }
+
private class TestFlow(val lastState: Bundle?) {
fun recreate(block: (FakeSavedStateRegistryOwner) -> Unit): TestFlow {
val fakeOwner = FakeSavedStateRegistryOwner()
diff --git a/savedstate/savedstate/src/main/java/androidx/savedstate/Recreator.java b/savedstate/savedstate/src/main/java/androidx/savedstate/Recreator.java
index 638eec2..33647d3 100644
--- a/savedstate/savedstate/src/main/java/androidx/savedstate/Recreator.java
+++ b/savedstate/savedstate/src/main/java/androidx/savedstate/Recreator.java
@@ -17,7 +17,6 @@
package androidx.savedstate;
-import android.annotation.SuppressLint;
import android.os.Bundle;
import androidx.annotation.NonNull;
@@ -31,7 +30,6 @@
import java.util.HashSet;
import java.util.Set;
-@SuppressLint("RestrictedApi")
final class Recreator implements LifecycleEventObserver {
static final String CLASSES_KEY = "classes_to_restore";
diff --git a/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistry.java b/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistry.java
index 68bdb26..a8a4ef2 100644
--- a/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistry.java
+++ b/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistry.java
@@ -42,6 +42,7 @@
private final SafeIterableMap<String, SavedStateProvider> mComponents =
new SafeIterableMap<>();
+ private boolean mAttached;
@Nullable
private Bundle mRestoredState;
private boolean mRestored;
@@ -180,17 +181,13 @@
}
/**
- * An interface for an owner of this @{code {@link SavedStateRegistry} to restore saved state.
- *
+ * An interface for an owner of this @{code {@link SavedStateRegistry} to attach this
+ * to a {@link Lifecycle}.
*/
- @SuppressWarnings("WeakerAccess")
@MainThread
- void performRestore(@NonNull Lifecycle lifecycle, @Nullable Bundle savedState) {
- if (mRestored) {
- throw new IllegalStateException("SavedStateRegistry was already restored.");
- }
- if (savedState != null) {
- mRestoredState = savedState.getBundle(SAVED_COMPONENTS_KEY);
+ void performAttach(@NonNull Lifecycle lifecycle) {
+ if (mAttached) {
+ throw new IllegalStateException("SavedStateRegistry was already attached.");
}
lifecycle.addObserver((LifecycleEventObserver) (source, event) -> {
@@ -201,6 +198,26 @@
}
});
+ mAttached = true;
+ }
+
+ /**
+ * An interface for an owner of this @{code {@link SavedStateRegistry} to restore saved state.
+ *
+ */
+ @MainThread
+ void performRestore(@Nullable Bundle savedState) {
+ if (!mAttached) {
+ throw new IllegalStateException("You must call performAttach() before calling "
+ + "performRestore(Bundle).");
+ }
+ if (mRestored) {
+ throw new IllegalStateException("SavedStateRegistry was already restored.");
+ }
+ if (savedState != null) {
+ mRestoredState = savedState.getBundle(SAVED_COMPONENTS_KEY);
+ }
+
mRestored = true;
}
diff --git a/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistryController.java b/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistryController.java
index 6fb25d2..98fe68f 100644
--- a/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistryController.java
+++ b/savedstate/savedstate/src/main/java/androidx/savedstate/SavedStateRegistryController.java
@@ -33,6 +33,8 @@
private final SavedStateRegistryOwner mOwner;
private final SavedStateRegistry mRegistry;
+ private boolean mAttached = false;
+
private SavedStateRegistryController(SavedStateRegistryOwner owner) {
mOwner = owner;
mRegistry = new SavedStateRegistry();
@@ -47,19 +49,42 @@
}
/**
- * An interface for an owner of this {@link SavedStateRegistry} to restore saved state.
- *
- * @param savedState restored state
+ * Perform the initial, one time attachment necessary to configure this
+ * {@link SavedStateRegistry}. This must be called when the owner's {@link Lifecycle} is
+ * {@link Lifecycle.State#INITIALIZED} and before you call
+ * {@link #performRestore(Bundle)}.
*/
@MainThread
- public void performRestore(@Nullable Bundle savedState) {
+ public void performAttach() {
Lifecycle lifecycle = mOwner.getLifecycle();
if (lifecycle.getCurrentState() != Lifecycle.State.INITIALIZED) {
throw new IllegalStateException("Restarter must be created only during "
+ "owner's initialization stage");
}
lifecycle.addObserver(new Recreator(mOwner));
- mRegistry.performRestore(lifecycle, savedState);
+ mRegistry.performAttach(lifecycle);
+
+ mAttached = true;
+ }
+
+ /**
+ * An interface for an owner of this {@link SavedStateRegistry} to restore saved state.
+ *
+ * @param savedState restored state
+ */
+ @MainThread
+ public void performRestore(@Nullable Bundle savedState) {
+ // To support backward compatibility with libraries that do not explicitly
+ // call performAttach(), we make sure that work is done here
+ if (!mAttached) {
+ performAttach();
+ }
+ Lifecycle lifecycle = mOwner.getLifecycle();
+ if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
+ throw new IllegalStateException("performRestore cannot be called when owner "
+ + " is " + lifecycle.getCurrentState());
+ }
+ mRegistry.performRestore(savedState);
}
/**
diff --git a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
index 7f59172..ad1ce6b 100644
--- a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
+++ b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
@@ -27,6 +27,16 @@
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
/**
+ * Default compilation modes to test for all AndroidX macrobenchmarks.
+ */
+val COMPILATION_MODES = listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.BaselineProfile,
+ CompilationMode.SpeedProfile()
+)
+
+/**
* Temporary, while transitioning to new metrics
*/
@RequiresApi(23)
@@ -64,11 +74,7 @@
StartupMode.WARM,
StartupMode.COLD
),
- compilationModes: List<CompilationMode> = listOf(
- CompilationMode.None,
- CompilationMode.Interpreted,
- CompilationMode.SpeedProfile()
- )
+ compilationModes: List<CompilationMode> = COMPILATION_MODES
): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
for (startupMode in startupModes) {
for (compilationMode in compilationModes) {
@@ -83,11 +89,7 @@
@RequiresApi(21)
fun createCompilationParams(
- compilationModes: List<CompilationMode> = listOf(
- CompilationMode.None,
- CompilationMode.Interpreted,
- CompilationMode.SpeedProfile()
- )
+ compilationModes: List<CompilationMode> = COMPILATION_MODES
): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
for (compilationMode in compilationModes) {
// Skip configs that can't run, so they don't clutter Studio benchmark
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/Common.kt b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
index f2d6b09..b60c1cc 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
@@ -16,13 +16,4 @@
package androidx.wear.compose.integration.macrobenchmark.test
-import androidx.benchmark.macro.CompilationMode
-
-internal val COMPOSE_COMPILATION_MODES = listOf(
- CompilationMode.None,
- CompilationMode.Interpreted,
- CompilationMode.SpeedProfile(),
- CompilationMode.BaselineProfile,
-)
-
internal val CONTENT_DESCRIPTION = "find-me"
\ No newline at end of file
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
index d547f80..453ae52 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
@@ -79,8 +79,6 @@
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun parameters() = createCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createCompilationParams()
}
}
\ No newline at end of file
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
index 967cc01..0b8754f 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
@@ -48,13 +48,6 @@
companion object {
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters() = createStartupCompilationParams(
- compilationModes = listOf(
- CompilationMode.None,
- CompilationMode.Interpreted,
- CompilationMode.SpeedProfile(),
- CompilationMode.BaselineProfile,
- )
- )
+ fun parameters() = createStartupCompilationParams()
}
}
\ No newline at end of file
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
index 03738ed..1721bec 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
@@ -79,8 +79,6 @@
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun parameters() = createCompilationParams(
- compilationModes = COMPOSE_COMPILATION_MODES
- )
+ fun parameters() = createCompilationParams()
}
}
\ No newline at end of file
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 435cb4e..552c556 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -32,6 +32,7 @@
import android.view.SurfaceHolder
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.screenshot.AndroidXScreenshotTestRule
import androidx.test.screenshot.assertAgainstGolden
@@ -676,6 +677,7 @@
assertThat(awaitWithTimeout(deferredInteractiveInstance2).instanceId).isEqualTo("testId")
}
+ @FlakyTest(bugId = 206646906)
@SuppressLint("NewApi") // renderWatchFaceToBitmap
@Test
fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance_styleChange() {
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 8bd3a06..b926b8c 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -563,6 +563,7 @@
)
}
+ @FlakyTest(bugId = 206648285)
@SuppressLint("NewApi")
@Test
public fun testCommandTakeOpenGLScreenShot() {
@@ -715,6 +716,7 @@
)
}
+ @FlakyTest(bugId = 206647510)
@SuppressLint("NewApi")
@Test
public fun testHighlightRightComplicationInScreenshot() {
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java
index 1a47757..bc4ffb3 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java
@@ -37,7 +37,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-@FlakyTest(bugId = 204342476)
+@FlakyTest(bugId = 204197604)
@LargeTest
@RunWith(AndroidJUnit4.class)
public class WebViewRenderProcessClientTest {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index ef51631..6ac25bd 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -47,7 +47,6 @@
* device, {@code null} otherwise. The implementation must match the API level reported in
* {@link WindowExtensions}.
* @return the OEM implementation of {@link WindowLayoutComponent}
- * @throws UnsupportedOperationException if the device does not support
*/
@Nullable
WindowLayoutComponent getWindowLayoutComponent();
diff --git a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
new file mode 100644
index 0000000..bfa5b7f
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.window.layout
+
+import androidx.window.extensions.WindowExtensionsProvider
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+/**
+ * An integration test to verify that if [WindowExtensionsProvider] is present then
+ * [SafeWindowLayoutComponentProvider.windowLayoutComponent] will return a value. This can fail if
+ * the implementation of window:extensions:extensions does not have the expected API.
+ */
+class SafeWindowLayoutComponentProviderTest {
+
+ /**
+ * Test that if [WindowExtensionsProvider] is available then
+ * [SafeWindowLayoutComponentProvider.windowLayoutComponent] returns a non-null value.
+ */
+ @Test
+ fun windowLayoutComponentIsAvailable_ifProviderIsAvailable() {
+ val safeComponent = SafeWindowLayoutComponentProvider.windowLayoutComponent
+
+ try {
+ val extensions = WindowExtensionsProvider.getWindowExtensions()
+ val actualComponent = extensions.windowLayoutComponent
+ if (actualComponent == null) {
+ assertNull(safeComponent)
+ } else {
+ assertNotNull(safeComponent)
+ }
+ } catch (e: UnsupportedOperationException) {
+ // Invalid implementation of extensions
+ assertNull(safeComponent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt
index 5909aaa..1657c65 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt
@@ -18,7 +18,6 @@
package androidx.window.layout
-import android.content.Context
import android.content.pm.ActivityInfo
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
@@ -26,6 +25,7 @@
import androidx.test.filters.LargeTest
import androidx.window.TestConfigChangeHandlingActivity
import androidx.window.WindowTestBase
+import androidx.window.core.SpecificationComputer.VerificationMode.QUIET
import androidx.window.core.Version
import androidx.window.layout.ExtensionInterfaceCompat.ExtensionCallbackInterface
import androidx.window.layout.HardwareFoldingFeature.Type
@@ -63,7 +63,9 @@
@Before
public fun setUp() {
assumeValidSidecar()
- sidecarCompat = SidecarCompat(ApplicationProvider.getApplicationContext() as Context)
+ val sidecar = SidecarCompat.getSidecarCompat(ApplicationProvider.getApplicationContext())
+ // TODO(b/206055949) convert to strict validation.
+ sidecarCompat = SidecarCompat(sidecar, SidecarAdapter(verificationMode = QUIET))
}
@Test
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/BenchmarkingUtils.kt b/window/window/src/debug/java/androidx/window/core/BuildConfig.kt
similarity index 60%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/BenchmarkingUtils.kt
rename to window/window/src/debug/java/androidx/window/core/BuildConfig.kt
index 253df9c..6dfe4b0 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/BenchmarkingUtils.kt
+++ b/window/window/src/debug/java/androidx/window/core/BuildConfig.kt
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package androidx.compose.integration.macrobenchmark
+package androidx.window.core
-import androidx.benchmark.macro.CompilationMode
+import androidx.window.core.SpecificationComputer.VerificationMode
+import androidx.window.core.SpecificationComputer.VerificationMode.STRICT
-internal val COMPOSE_COMPILATION_MODES = listOf(
- CompilationMode.None,
- CompilationMode.Interpreted,
- CompilationMode.SpeedProfile(),
- CompilationMode.BaselineProfile,
-)
\ No newline at end of file
+/**
+ * A configuration to be used for the debug flavor of the library. Default [VerificationMode] is
+ * [STRICT] so to surface errors.
+ */
+internal object BuildConfig {
+ val verificationMode: VerificationMode = STRICT
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/core/SpecificationComputer.kt b/window/window/src/main/java/androidx/window/core/SpecificationComputer.kt
new file mode 100644
index 0000000..70b7623
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/core/SpecificationComputer.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.window.core
+
+import android.util.Log
+import androidx.window.core.SpecificationComputer.VerificationMode.LOG
+import androidx.window.core.SpecificationComputer.VerificationMode.QUIET
+import androidx.window.core.SpecificationComputer.VerificationMode.STRICT
+
+/**
+ * An [Exception] to signal that strict mode has been violated. This class should only be used
+ * within the [SpecificationComputer]
+ */
+internal class WindowStrictModeException(message: String) : Exception(message)
+
+/**
+ * The root abstract class to represent a [SpecificationComputer]. A [SpecificationComputer] can be
+ * used to validate assumptions about objects and interfaces when working with OEM provided
+ * libraries. The [SpecificationComputer] has three verification modes, [STRICT], [LOG], and
+ * [QUIET]. For [STRICT] mode, the [SpecificationComputer] will throw an exception when a required
+ * condition is not met. For [LOG] mode, the [SpecificationComputer] will create a debug log when a
+ * required condition is not met. For [QUIET] mode, value is simply converted to null if the
+ * specification is not met.
+ */
+internal abstract class SpecificationComputer<T : Any> {
+ /**
+ * An enum to specify which [VerificationMode] should be used with the [SpecificationComputer]
+ */
+ enum class VerificationMode {
+ STRICT,
+ LOG,
+ QUIET
+ }
+
+ /**
+ * Checks if the required condition is met and returns a [SpecificationComputer] based on the
+ * result. If the condition is met then the [SpecificationComputer] that is returned will
+ * continue to process additional checks. If the condition fails then the
+ * [SpecificationComputer] will have different behavior depending on the verification mode. If
+ * the verification mode is [STRICT] an [Exception] will be thrown when [compute] is
+ * called. If the verification mode is [LOG] a debug log will be written. If the verification
+ * mode is [QUIET] then an empty [SpecificationComputer] is returned.
+ *
+ * Use [require] when an assumption must be true. One key example is when translating from an
+ * OEM provided library into a local object you want to verify the attributes. If an attribute
+ * does not match expectations then this can raise an exception at the source.
+ *
+ * One use case for this is our integration tests for devices. We can write good assumptions in
+ * a test because we would need external knowledge of the device. One example is a device with a
+ * fold. We expect the fold to propagate through the translation layer but we can only assert
+ * the presence of a fold if we know the device has a fold. As a substitution we can turn on
+ * strict mode and this will throw an exception when translating causing the test to fail.
+ *
+ * @param message a description of what the condition is testing. This should be user readable
+ * @param condition a test that the current value is expected to pass.
+ * @return A [SpecificationComputer] that will continue computing if the condition was met or
+ * skip subsequent checks if the condition was not met.
+ */
+ abstract fun require(message: String, condition: T.() -> Boolean): SpecificationComputer<T>
+
+ /**
+ * Computes the result of the previous checks and the returned value depends on the
+ * [VerificationMode]. For [STRICT] mode an [Exception] may be thrown if the condition was
+ * required. For [LOG] mode a debug log is written to the [Logger]. For [QUIET] mode there are
+ * no side effects.
+ * @return The value if all the checks passed or none of the checks were required, null
+ * otherwise.
+ * @throws WindowStrictModeException if [STRICT] mode is set and a required check fails.
+ */
+ abstract fun compute(): T?
+
+ protected fun createMessage(value: Any, message: String): String {
+ return "$message value: $value"
+ }
+
+ companion object {
+ /**
+ * Start a specification for the receiver.
+ *
+ * @param T a non-null value that the specification will run against.
+ * @param tag an identifier that will be used when writing debug logs.
+ * @param verificationMode determines if the checks should throw, log, or fail silently.
+ * @param logger a [Logger] that can be substituted for testing purposes. The default
+ * value writes a log to LogCat
+ * @return A [SpecificationComputer] that is initially valid and can check other
+ * conditions.
+ */
+ fun <T : Any> T.startSpecification(
+ tag: String,
+ verificationMode: VerificationMode = BuildConfig.verificationMode,
+ logger: Logger = AndroidLogger
+ ): SpecificationComputer<T> {
+ return ValidSpecification(this, tag, verificationMode, logger)
+ }
+ }
+}
+
+/**
+ * A [SpecificationComputer] where the value has passed all previous checks.
+ */
+private class ValidSpecification<T : Any>(
+ val value: T,
+ val tag: String,
+ val verificationMode: VerificationMode,
+ val logger: Logger
+) : SpecificationComputer<T>() {
+
+ override fun require(message: String, condition: T.() -> Boolean): SpecificationComputer<T> {
+ return if (condition(value)) {
+ this
+ } else {
+ FailedSpecification(
+ value = value,
+ tag = tag,
+ message = message,
+ logger = logger,
+ verificationMode = verificationMode
+ )
+ }
+ }
+
+ override fun compute(): T {
+ return value
+ }
+}
+
+/**
+ * A [SpecificationComputer] that has failed a required check
+ */
+private class FailedSpecification<T : Any>(
+ val value: T,
+ val tag: String,
+ val message: String,
+ val logger: Logger,
+ val verificationMode: VerificationMode
+) : SpecificationComputer<T>() {
+
+ val exception: WindowStrictModeException =
+ WindowStrictModeException(createMessage(value, message)).apply {
+ stackTrace = stackTrace.drop(2).toTypedArray()
+ }
+
+ override fun require(message: String, condition: T.() -> Boolean): SpecificationComputer<T> {
+ return this
+ }
+
+ override fun compute(): T? {
+ return when (verificationMode) {
+ STRICT -> throw exception
+ LOG -> {
+ logger.debug(tag, createMessage(value, message))
+ null
+ }
+ QUIET -> null
+ }
+ }
+}
+
+internal interface Logger {
+ fun debug(tag: String, message: String)
+}
+
+internal object AndroidLogger : Logger {
+ override fun debug(tag: String, message: String) {
+ Log.d(tag, message)
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
new file mode 100644
index 0000000..6785ecf
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.window.layout
+
+import android.app.Activity
+import android.graphics.Rect
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.window.extensions.WindowExtensionsProvider
+import androidx.window.extensions.layout.WindowLayoutComponent
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.util.function.Consumer
+import kotlin.reflect.KClass
+
+internal object SafeWindowLayoutComponentProvider {
+
+ val windowLayoutComponent: WindowLayoutComponent? by lazy {
+ val loader = SafeWindowLayoutComponentProvider::class.java.classLoader
+ if (loader != null && canUseWindowLayoutComponent(loader)) {
+ try {
+ WindowExtensionsProvider.getWindowExtensions().windowLayoutComponent
+ } catch (e: UnsupportedOperationException) {
+ null
+ }
+ } else {
+ null
+ }
+ }
+
+ private fun canUseWindowLayoutComponent(classLoader: ClassLoader): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ isWindowLayoutProviderValid(classLoader) &&
+ isWindowExtensionsValid(classLoader) &&
+ isWindowLayoutComponentValid(classLoader) &&
+ isFoldingFeatureValid(classLoader)
+ } else {
+ false
+ }
+ }
+
+ private fun isWindowLayoutProviderValid(classLoader: ClassLoader): Boolean {
+ return validate {
+ val providerClass = windowExtensionsProviderClass(classLoader)
+ val getWindowExtensionsMethod = providerClass.getDeclaredMethod("getWindowExtensions")
+ val windowExtensionsClass = windowExtensionsClass(classLoader)
+ getWindowExtensionsMethod.doesReturn(windowExtensionsClass) &&
+ getWindowExtensionsMethod.isPublic
+ }
+ }
+
+ private fun isWindowExtensionsValid(classLoader: ClassLoader): Boolean {
+ return validate {
+ val extensionsClass = windowExtensionsClass(classLoader)
+ val getWindowLayoutComponentMethod =
+ extensionsClass.getMethod("getWindowLayoutComponent")
+ val windowLayoutComponentClass = windowLayoutComponentClass(classLoader)
+ getWindowLayoutComponentMethod.isPublic &&
+ getWindowLayoutComponentMethod.doesReturn(windowLayoutComponentClass)
+ }
+ }
+
+ private fun isFoldingFeatureValid(classLoader: ClassLoader): Boolean {
+ return validate {
+ val foldingFeatureClass = foldingFeatureClass(classLoader)
+ val getBoundsMethod = foldingFeatureClass.getMethod("getBounds")
+ val getTypeMethod = foldingFeatureClass.getMethod("getType")
+ val getStateMethod = foldingFeatureClass.getMethod("getState")
+ getBoundsMethod.doesReturn(Rect::class) &&
+ getBoundsMethod.isPublic &&
+ getTypeMethod.doesReturn(Int::class) &&
+ getTypeMethod.isPublic &&
+ getStateMethod.doesReturn(Int::class) &&
+ getStateMethod.isPublic
+ }
+ }
+
+ @RequiresApi(24)
+ private fun isWindowLayoutComponentValid(classLoader: ClassLoader): Boolean {
+ return validate {
+ val windowLayoutComponent = windowLayoutComponentClass(classLoader)
+ val addListenerMethod = windowLayoutComponent
+ .getMethod(
+ "addWindowLayoutInfoListener",
+ Activity::class.java,
+ Consumer::class.java
+ )
+ val removeListenerMethod = windowLayoutComponent
+ .getMethod("removeWindowLayoutInfoListener", Consumer::class.java)
+ addListenerMethod.isPublic && removeListenerMethod.isPublic
+ }
+ }
+
+ private fun validate(block: () -> Boolean): Boolean {
+ return try {
+ block()
+ } catch (noClass: ClassNotFoundException) {
+ false
+ } catch (noMethod: NoSuchMethodException) {
+ false
+ }
+ }
+
+ private val Method.isPublic: Boolean
+ get() {
+ return Modifier.isPublic(modifiers)
+ }
+
+ private fun Method.doesReturn(clazz: KClass<*>): Boolean {
+ return doesReturn(clazz.java)
+ }
+
+ private fun Method.doesReturn(clazz: Class<*>): Boolean {
+ return returnType.equals(clazz)
+ }
+
+ private fun windowExtensionsProviderClass(classLoader: ClassLoader) =
+ classLoader.loadClass("androidx.window.extensions.WindowExtensionsProvider")
+
+ private fun windowExtensionsClass(classLoader: ClassLoader) =
+ classLoader.loadClass("androidx.window.extensions.WindowExtensions")
+
+ private fun foldingFeatureClass(classLoader: ClassLoader) =
+ classLoader.loadClass("androidx.window.extensions.layout.FoldingFeature")
+
+ private fun windowLayoutComponentClass(classLoader: ClassLoader) =
+ classLoader.loadClass("androidx.window.extensions.layout.WindowLayoutComponent")
+}
diff --git a/window/window/src/main/java/androidx/window/layout/SidecarAdapter.kt b/window/window/src/main/java/androidx/window/layout/SidecarAdapter.kt
index 75624d8..b51d717 100644
--- a/window/window/src/main/java/androidx/window/layout/SidecarAdapter.kt
+++ b/window/window/src/main/java/androidx/window/layout/SidecarAdapter.kt
@@ -18,19 +18,26 @@
package androidx.window.layout
import android.annotation.SuppressLint
-import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.window.core.Bounds
+import androidx.window.core.SpecificationComputer.Companion.startSpecification
+import androidx.window.core.SpecificationComputer.VerificationMode
+import androidx.window.core.SpecificationComputer.VerificationMode.QUIET
+import androidx.window.layout.HardwareFoldingFeature.Type.Companion.FOLD
+import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
import androidx.window.layout.SidecarWindowBackend.Companion.DEBUG
import androidx.window.sidecar.SidecarDeviceState
import androidx.window.sidecar.SidecarDisplayFeature
+import androidx.window.sidecar.SidecarDisplayFeature.TYPE_FOLD
+import androidx.window.sidecar.SidecarDisplayFeature.TYPE_HINGE
import androidx.window.sidecar.SidecarWindowLayoutInfo
import java.lang.reflect.InvocationTargetException
/**
* A class for translating Sidecar data classes.
*/
-internal class SidecarAdapter {
+// TODO(b/206055949) convert to strict validation.
+internal class SidecarAdapter(private val verificationMode: VerificationMode = QUIET) {
fun translate(
sidecarDisplayFeatures: List<SidecarDisplayFeature>,
@@ -141,6 +148,48 @@
}
}
+ /**
+ * Converts the display feature from extension. Can return `null` if there is an issue
+ * with the value passed from extension.
+ */
+ internal fun translate(
+ feature: SidecarDisplayFeature,
+ deviceState: SidecarDeviceState
+ ): DisplayFeature? {
+ val checkedFeature = feature.startSpecification(TAG, verificationMode)
+ .require("Type must be either TYPE_FOLD or TYPE_HINGE") {
+ type == TYPE_FOLD || type == TYPE_HINGE
+ }
+ .require("Feature bounds must not be 0") { rect.width() != 0 || rect.height() != 0 }
+ .require("TYPE_FOLD must have 0 area") {
+ if (type == TYPE_FOLD) {
+ rect.width() == 0 || rect.height() == 0
+ } else {
+ true
+ }
+ }
+ .require("Feature be pinned to either left or top") {
+ rect.left == 0 || rect.top == 0
+ }
+ .compute() ?: return null
+ val type = when (checkedFeature.type) {
+ TYPE_FOLD -> FOLD
+ TYPE_HINGE -> HINGE
+ else -> {
+ return null
+ }
+ }
+ val state = when (getSidecarDevicePosture(deviceState)) {
+ SidecarDeviceState.POSTURE_CLOSED,
+ SidecarDeviceState.POSTURE_UNKNOWN,
+ SidecarDeviceState.POSTURE_FLIPPED -> return null
+ SidecarDeviceState.POSTURE_HALF_OPENED -> FoldingFeature.State.HALF_OPENED
+ SidecarDeviceState.POSTURE_OPENED -> FoldingFeature.State.FLAT
+ else -> FoldingFeature.State.FLAT
+ }
+ return HardwareFoldingFeature(Bounds(feature.rect), type, state)
+ }
+
companion object {
private val TAG = SidecarAdapter::class.java.simpleName
@@ -271,77 +320,5 @@
}
}
}
-
- /**
- * Converts the display feature from extension. Can return `null` if there is an issue
- * with the value passed from extension.
- */
- internal fun translate(
- feature: SidecarDisplayFeature,
- deviceState: SidecarDeviceState
- ): DisplayFeature? {
- val bounds = feature.rect
- if (bounds.width() == 0 && bounds.height() == 0) {
- if (DEBUG) {
- Log.d(TAG, "Passed a display feature with empty rect, skipping: $feature")
- }
- return null
- }
- if (feature.type == SidecarDisplayFeature.TYPE_FOLD) {
- if (bounds.width() != 0 && bounds.height() != 0) {
- // Bounds for fold types are expected to be zero-wide or zero-high.
- // See DisplayFeature#getBounds().
- if (DEBUG) {
- Log.d(
- TAG,
- "Passed a non-zero area display feature expected to be zero-area, " +
- "skipping: $feature"
- )
- }
- return null
- }
- }
- if (feature.type == SidecarDisplayFeature.TYPE_HINGE ||
- feature.type == SidecarDisplayFeature.TYPE_FOLD
- ) {
- // TODO(b/175507310): Reinstate after fix on the OEM side.
- if (
- !(
- bounds.left == 0 /* && bounds.right == windowBounds.width()*/ ||
- bounds.top == 0 /* && bounds.bottom == windowBounds.height()*/
- )
- ) {
- // Bounds for fold and hinge types are expected to span the entire window space.
- // See DisplayFeature#getBounds().
- if (DEBUG) {
- Log.d(
- TAG,
- "Passed a display feature expected to span the entire window but " +
- "does not, skipping: $feature"
- )
- }
- return null
- }
- }
- val type = when (feature.type) {
- SidecarDisplayFeature.TYPE_FOLD -> HardwareFoldingFeature.Type.FOLD
- SidecarDisplayFeature.TYPE_HINGE -> HardwareFoldingFeature.Type.HINGE
- else -> {
- if (DEBUG) {
- Log.d(TAG, "Unknown feature type: ${feature.type}, skipping feature.")
- }
- return null
- }
- }
- val state = when (getSidecarDevicePosture(deviceState)) {
- SidecarDeviceState.POSTURE_CLOSED,
- SidecarDeviceState.POSTURE_UNKNOWN,
- SidecarDeviceState.POSTURE_FLIPPED -> return null
- SidecarDeviceState.POSTURE_HALF_OPENED -> FoldingFeature.State.HALF_OPENED
- SidecarDeviceState.POSTURE_OPENED -> FoldingFeature.State.FLAT
- else -> FoldingFeature.State.FLAT
- }
- return HardwareFoldingFeature(Bounds(feature.rect), type, state)
- }
}
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt b/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt
index 78131e5..4548540 100644
--- a/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt
+++ b/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt
@@ -60,7 +60,7 @@
private var extensionCallback: ExtensionCallbackInterface? = null
constructor(context: Context) : this(
- SidecarProvider.getSidecarImpl(context.applicationContext),
+ getSidecarCompat(context),
SidecarAdapter()
)
@@ -456,6 +456,10 @@
null
}
+ internal fun getSidecarCompat(context: Context): SidecarInterface? {
+ return SidecarProvider.getSidecarImpl(context.applicationContext)
+ }
+
/**
* A utility method [Activity] to return an optional [IBinder] window token from an
* [Activity].
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
index 8ff4ba2..683c043 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
@@ -21,7 +21,6 @@
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
-import androidx.window.extensions.WindowExtensionsProvider
import kotlinx.coroutines.flow.Flow
/**
@@ -75,7 +74,7 @@
@Suppress("MemberVisibilityCanBePrivate") // Avoid synthetic accessor
internal fun windowBackend(context: Context): WindowBackend {
val extensionBackend = try {
- WindowExtensionsProvider.getWindowExtensions().windowLayoutComponent
+ SafeWindowLayoutComponentProvider.windowLayoutComponent
?.let(::ExtensionWindowLayoutInfoBackend)
} catch (t: Throwable) {
if (DEBUG) {
diff --git a/window/window/src/release/java/androidx/window/core/BuildConfig.kt b/window/window/src/release/java/androidx/window/core/BuildConfig.kt
new file mode 100644
index 0000000..7706338
--- /dev/null
+++ b/window/window/src/release/java/androidx/window/core/BuildConfig.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.window.core
+
+import androidx.window.core.SpecificationComputer.VerificationMode
+import androidx.window.core.SpecificationComputer.VerificationMode.QUIET
+
+/**
+ * A configuration to be used for the release flavor of the library. Default [VerificationMode] is
+ * [QUIET] so that apps do not crash when there are errors that we can handle.
+ */
+internal object BuildConfig {
+ val verificationMode: VerificationMode = QUIET
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/core/SpecificationComputerTest.kt b/window/window/src/test/java/androidx/window/core/SpecificationComputerTest.kt
new file mode 100644
index 0000000..bf12475
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/core/SpecificationComputerTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.window.core
+
+import androidx.window.core.SpecificationComputer.Companion.startSpecification
+import androidx.window.core.SpecificationComputer.VerificationMode.LOG
+import androidx.window.core.SpecificationComputer.VerificationMode.QUIET
+import androidx.window.core.SpecificationComputer.VerificationMode.STRICT
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.verify
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.ArgumentMatchers.contains
+
+/**
+ * Tests [SpecificationComputer] to verify the following behaviors.
+ * The key use-cases are when [SpecificationComputer.require] has failed and the expected behaviors
+ * are as follows. For [STRICT] mode, [SpecificationComputer.compute] should throw an exception.
+ * For [QUIET] mode, [SpecificationComputer.compute] should return a null value. For [LOG] mode,
+ * [SpecificationComputer.compute] should write a log and return a null value.
+ */
+class SpecificationComputerTest {
+
+ @Test
+ fun run_returnsValidValue() {
+ val actual = DATA.startSpecification(TAG, QUIET).compute()
+
+ assertEquals(DATA, actual)
+ }
+
+ @Test
+ fun run_returnsNullIfRequiredCheckFails() {
+ val actual = DATA.startSpecification(TAG, QUIET)
+ .require(MESSAGE) { this.isEmpty() }
+ .compute()
+
+ assertNull(actual)
+ }
+
+ @Test(expected = WindowStrictModeException::class)
+ fun run_throwsIfStrictModeEnabled() {
+ DATA.startSpecification(TAG, STRICT)
+ .require(MESSAGE) { this.isEmpty() }
+ .compute()
+ }
+
+ @Test
+ fun run_exceptionStackAtRunCaller() {
+ var actual: WindowStrictModeException? = null
+ try {
+ DATA.startSpecification(TAG, STRICT)
+ .require(MESSAGE) { this.isEmpty() }
+ .compute()
+ } catch (e: WindowStrictModeException) {
+ actual = e
+ }
+ assertNotNull(actual)
+ assertEquals("run_exceptionStackAtRunCaller", actual!!.stackTrace.first().methodName)
+ assertTrue(actual.message!!.contains(MESSAGE))
+ }
+
+ @Test
+ fun run_logsIfLoggingModeEnabled() {
+ val logger = mock<Logger>()
+ DATA.startSpecification(TAG, LOG, logger)
+ .require(MESSAGE) { this.isEmpty() }
+ .compute()
+
+ verify(logger).debug(eq(TAG), contains(MESSAGE))
+ }
+
+ @Test
+ fun check_logsIfLoggingModeEnabled() {
+ val logger = mock<Logger>()
+ DATA.startSpecification(TAG, LOG, logger)
+ .require(MESSAGE) { this.isEmpty() }
+ .compute()
+
+ verify(logger).debug(eq(TAG), contains(MESSAGE))
+ }
+
+ companion object {
+ const val DATA = "test-data-23849573"
+ const val TAG = "test-tag-3462353414123412"
+ const val MESSAGE = "test-message-123412431241235"
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/SchedulersTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/SchedulersTest.java
index d964c4e..c1d5d98 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/SchedulersTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/SchedulersTest.java
@@ -28,6 +28,7 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
import androidx.work.impl.background.systemalarm.SystemAlarmScheduler;
@@ -52,6 +53,7 @@
mAppContext = ApplicationProvider.getApplicationContext();
}
+ @FlakyTest(bugId = 206647994)
@Test
@SdkSuppress(minSdkVersion = WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
public void testGetBackgroundScheduler_withJobSchedulerApiLevel() {