Merge "Migrating code from systemui to common library." into udc-qpr-dev
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
// Copyright (C) 2022 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 {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "SystemUIComposeTesting",
|
||||
manifest: "AndroidManifest.xml",
|
||||
|
||||
srcs: [
|
||||
"src/**/*.kt",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"PlatformComposeCore",
|
||||
"SystemUIScreenshotLib",
|
||||
|
||||
"androidx.compose.runtime_runtime",
|
||||
"androidx.compose.material3_material3",
|
||||
"androidx.compose.ui_ui-test-junit4",
|
||||
"androidx.compose.ui_ui-test-manifest",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.android.systemui.testing.compose">
|
||||
<application
|
||||
android:appComponentFactory="androidx.core.app.AppComponentFactory"
|
||||
tools:replace="android:appComponentFactory">
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.compose
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ViewRootForTest
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import com.android.compose.theme.PlatformTheme
|
||||
import com.android.systemui.testing.screenshot.ScreenshotActivity
|
||||
import com.android.systemui.testing.screenshot.SystemUIGoldenImagePathManager
|
||||
import com.android.systemui.testing.screenshot.UnitTestBitmapMatcher
|
||||
import com.android.systemui.testing.screenshot.drawIntoBitmap
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import platform.test.screenshot.DeviceEmulationRule
|
||||
import platform.test.screenshot.DeviceEmulationSpec
|
||||
import platform.test.screenshot.MaterialYouColorsRule
|
||||
import platform.test.screenshot.ScreenshotTestRule
|
||||
import platform.test.screenshot.getEmulatedDevicePathConfig
|
||||
|
||||
/** A rule for Compose screenshot diff tests. */
|
||||
class ComposeScreenshotTestRule(
|
||||
emulationSpec: DeviceEmulationSpec,
|
||||
assetPathRelativeToBuildRoot: String
|
||||
) : TestRule {
|
||||
private val colorsRule = MaterialYouColorsRule()
|
||||
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
|
||||
private val screenshotRule =
|
||||
ScreenshotTestRule(
|
||||
SystemUIGoldenImagePathManager(
|
||||
getEmulatedDevicePathConfig(emulationSpec),
|
||||
assetPathRelativeToBuildRoot
|
||||
)
|
||||
)
|
||||
private val composeRule = createAndroidComposeRule<ScreenshotActivity>()
|
||||
private val delegateRule =
|
||||
RuleChain.outerRule(colorsRule)
|
||||
.around(deviceEmulationRule)
|
||||
.around(screenshotRule)
|
||||
.around(composeRule)
|
||||
private val matcher = UnitTestBitmapMatcher
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return delegateRule.apply(base, description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare [content] with the golden image identified by [goldenIdentifier] in the context of
|
||||
* [testSpec].
|
||||
*/
|
||||
fun screenshotTest(
|
||||
goldenIdentifier: String,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
// Make sure that the activity draws full screen and fits the whole display instead of the
|
||||
// system bars.
|
||||
val activity = composeRule.activity
|
||||
activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) }
|
||||
|
||||
// Set the content using the AndroidComposeRule to make sure that the Activity is set up
|
||||
// correctly.
|
||||
composeRule.setContent {
|
||||
PlatformTheme {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
|
||||
val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view
|
||||
screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (C) 2022 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 {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "SystemUIScreenshotLib",
|
||||
manifest: "AndroidManifest.xml",
|
||||
|
||||
srcs: [
|
||||
"src/**/*.kt",
|
||||
],
|
||||
|
||||
resource_dirs: [
|
||||
"res",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SystemUI-core",
|
||||
"androidx.test.espresso.core",
|
||||
"androidx.appcompat_appcompat",
|
||||
"platform-screenshot-diff-core",
|
||||
"guava",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.systemui.testing.screenshot">
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.android.systemui.testing.screenshot.ScreenshotActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SystemUI.Screenshot" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI">
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
|
||||
<!-- We make the status and navigation bars transparent so that the screenshotted content is
|
||||
not clipped by the status bar height when drawn into the Bitmap (which is what happens
|
||||
given that we draw the view into the Bitmap using hardware acceleration). -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
||||
<!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import platform.test.screenshot.matchers.MSSIMMatcher
|
||||
import platform.test.screenshot.matchers.PixelPerfectMatcher
|
||||
|
||||
/** Draw this [View] into a [Bitmap]. */
|
||||
// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their
|
||||
// tests.
|
||||
fun View.drawIntoBitmap(): Bitmap {
|
||||
val bitmap =
|
||||
Bitmap.createBitmap(
|
||||
measuredWidth,
|
||||
measuredHeight,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
|
||||
* screenshot *unit* tests.
|
||||
*/
|
||||
val UnitTestBitmapMatcher =
|
||||
if (Build.CPU_ABI == "x86_64") {
|
||||
// Different CPU architectures can sometimes end up rendering differently, so we can't do
|
||||
// pixel-perfect matching on different architectures using the same golden. Given that our
|
||||
// presubmits are run on cf_x86_64_phone, our goldens should be perfectly matched on the
|
||||
// x86_64 architecture and use the Structural Similarity Index on others.
|
||||
// TODO(b/237511747): Run our screenshot presubmit tests on arm64 instead so that we can
|
||||
// do pixel perfect matching both at presubmit time and at development time with actual
|
||||
// devices.
|
||||
PixelPerfectMatcher()
|
||||
} else {
|
||||
MSSIMMatcher()
|
||||
}
|
||||
|
||||
/**
|
||||
* The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
|
||||
* screenshot *unit* tests.
|
||||
*
|
||||
* We use the Structural Similarity Index for integration tests because they usually contain
|
||||
* additional information and noise that shouldn't break the test.
|
||||
*/
|
||||
val IntegrationTestBitmapMatcher = MSSIMMatcher()
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import platform.test.screenshot.*
|
||||
|
||||
/**
|
||||
* A rule that allows to run a screenshot diff test on a view that is hosted in another activity.
|
||||
*/
|
||||
class ExternalViewScreenshotTestRule(
|
||||
emulationSpec: DeviceEmulationSpec,
|
||||
assetPathRelativeToBuildRoot: String
|
||||
) : TestRule {
|
||||
|
||||
private val colorsRule = MaterialYouColorsRule()
|
||||
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
|
||||
private val screenshotRule =
|
||||
ScreenshotTestRule(
|
||||
SystemUIGoldenImagePathManager(
|
||||
getEmulatedDevicePathConfig(emulationSpec),
|
||||
assetPathRelativeToBuildRoot
|
||||
)
|
||||
)
|
||||
private val delegateRule =
|
||||
RuleChain.outerRule(colorsRule).around(deviceEmulationRule).around(screenshotRule)
|
||||
private val matcher = UnitTestBitmapMatcher
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return delegateRule.apply(base, description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the content of the [view] with the golden image identified by [goldenIdentifier] in
|
||||
* the context of [emulationSpec]. Window must be specified to capture views that render
|
||||
* hardware buffers.
|
||||
*/
|
||||
fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) {
|
||||
view.removeElevationRecursively()
|
||||
|
||||
ScreenshotRuleAsserter.Builder(screenshotRule)
|
||||
.setScreenshotProvider { view.toBitmap(window) }
|
||||
.withMatcher(matcher)
|
||||
.build()
|
||||
.assertGoldenImage(goldenIdentifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the content of the [activity] with the golden image identified by [goldenIdentifier]
|
||||
* in the context of [emulationSpec].
|
||||
*/
|
||||
fun activityScreenshotTest(
|
||||
goldenIdentifier: String,
|
||||
activity: Activity,
|
||||
) {
|
||||
val rootView = activity.window.decorView
|
||||
|
||||
// Hide system bars, remove insets, focus and make sure device-specific cutouts
|
||||
// don't affect screenshots
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
val window = activity.window
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
WindowInsetsControllerCompat(window, rootView).apply {
|
||||
hide(WindowInsetsCompat.Type.systemBars())
|
||||
systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
window.attributes =
|
||||
window.attributes.apply {
|
||||
layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
|
||||
rootView.removeInsetsRecursively()
|
||||
activity.currentFocus?.clearFocus()
|
||||
}
|
||||
|
||||
screenshotTest(goldenIdentifier, rootView, activity.window)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
/** The Activity that is launched and whose content is set for screenshot tests. */
|
||||
class ScreenshotActivity : ComponentActivity()
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import platform.test.screenshot.GoldenImagePathManager
|
||||
import platform.test.screenshot.PathConfig
|
||||
|
||||
/** A [GoldenImagePathManager] that should be used for all SystemUI screenshot tests. */
|
||||
class SystemUIGoldenImagePathManager(
|
||||
pathConfig: PathConfig,
|
||||
assetsPathRelativeToBuildRoot: String
|
||||
) :
|
||||
GoldenImagePathManager(
|
||||
appContext = InstrumentationRegistry.getInstrumentation().context,
|
||||
assetsPathRelativeToBuildRoot = assetsPathRelativeToBuildRoot,
|
||||
deviceLocalPath =
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.filesDir
|
||||
.absolutePath
|
||||
.toString() + "/sysui_screenshots",
|
||||
pathConfig = pathConfig,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
// This string is appended to all actual/expected screenshots on the device, so make sure
|
||||
// it is a static value.
|
||||
return "SystemUIGoldenImagePathManager"
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.core.app.AppComponentFactory
|
||||
|
||||
class TestAppComponentFactory : AppComponentFactory() {
|
||||
|
||||
init {
|
||||
instance = this
|
||||
}
|
||||
|
||||
private val overrides: MutableMap<String, () -> Activity> = hashMapOf()
|
||||
|
||||
fun clearOverrides() {
|
||||
overrides.clear()
|
||||
}
|
||||
|
||||
fun <T : Activity> registerActivityOverride(activity: Class<T>, provider: () -> T) {
|
||||
overrides[activity.name] = provider
|
||||
}
|
||||
|
||||
override fun instantiateActivityCompat(
|
||||
cl: ClassLoader,
|
||||
className: String,
|
||||
intent: Intent?
|
||||
): Activity {
|
||||
return overrides
|
||||
.getOrDefault(className) { super.instantiateActivityCompat(cl, className, intent) }
|
||||
.invoke()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: TestAppComponentFactory? = null
|
||||
|
||||
fun getInstance(): TestAppComponentFactory =
|
||||
instance
|
||||
?: error(
|
||||
"TestAppComponentFactory is not initialized, " +
|
||||
"did you specify it in the manifest?"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.android.systemui.util.children
|
||||
import android.view.WindowInsets
|
||||
|
||||
/**
|
||||
* Elevation/shadows is not deterministic when doing hardware rendering, this exentsion allows to
|
||||
* disable it for any view in the hierarchy.
|
||||
*/
|
||||
fun View.removeElevationRecursively() {
|
||||
this.elevation = 0f
|
||||
(this as? ViewGroup)?.children?.forEach(View::removeElevationRecursively)
|
||||
}
|
||||
|
||||
/**
|
||||
* Different devices could have different insets (e.g. different height of the navigation bar or
|
||||
* taskbar). This method dispatches empty insets to the whole view hierarchy and removes
|
||||
* the original listener, so the views won't receive real insets.
|
||||
*/
|
||||
fun View.removeInsetsRecursively() {
|
||||
this.dispatchApplyWindowInsets(WindowInsets.CONSUMED)
|
||||
this.setOnApplyWindowInsetsListener(null)
|
||||
(this as? ViewGroup)?.children?.forEach(View::removeInsetsRecursively)
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package com.android.systemui.testing.screenshot
|
||||
|
||||
import android.annotation.WorkerThread
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.HardwareRenderer
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.PixelCopy
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.Window
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.concurrent.futures.ResolvableFuture
|
||||
import androidx.test.annotation.ExperimentalTestApi
|
||||
import androidx.test.core.internal.os.HandlerExecutor
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.platform.graphics.HardwareRendererCompat
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/*
|
||||
* This file was forked from androidx/test/core/view/ViewCapture.kt to add [Window] parameter to
|
||||
* [View.captureToBitmap].
|
||||
* TODO(b/195673633): Remove this fork and use the AndroidX version instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Asynchronously captures an image of the underlying view into a [Bitmap].
|
||||
*
|
||||
* For devices below [Build.VERSION_CODES#O] (or if the view's window cannot be determined), the
|
||||
* image is obtained using [View#draw]. Otherwise, [PixelCopy] is used.
|
||||
*
|
||||
* This method will also enable [HardwareRendererCompat#setDrawingEnabled(boolean)] if required.
|
||||
*
|
||||
* This API is primarily intended for use in lower layer libraries or frameworks. For test authors,
|
||||
* its recommended to use espresso or compose's captureToImage.
|
||||
*
|
||||
* This API is currently experimental and subject to change or removal.
|
||||
*/
|
||||
@ExperimentalTestApi
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
fun View.captureToBitmap(window: Window? = null): ListenableFuture<Bitmap> {
|
||||
val bitmapFuture: ResolvableFuture<Bitmap> = ResolvableFuture.create()
|
||||
val mainExecutor = HandlerExecutor(Handler(Looper.getMainLooper()))
|
||||
val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false
|
||||
|
||||
// disable drawing again if necessary once work is complete
|
||||
if (!HardwareRendererCompat.isDrawingEnabled()) {
|
||||
HardwareRendererCompat.setDrawingEnabled(true)
|
||||
bitmapFuture.addListener({ HardwareRendererCompat.setDrawingEnabled(false) }, mainExecutor)
|
||||
}
|
||||
|
||||
mainExecutor.execute {
|
||||
if (isRobolectric) {
|
||||
generateBitmap(bitmapFuture)
|
||||
} else {
|
||||
val forceRedrawFuture = forceRedraw()
|
||||
forceRedrawFuture.addListener({ generateBitmap(bitmapFuture, window) }, mainExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
return bitmapFuture
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously captures an image of the view into a [Bitmap]. Synchronous equivalent of
|
||||
* [captureToBitmap].
|
||||
*/
|
||||
@WorkerThread
|
||||
@ExperimentalTestApi
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
fun View.toBitmap(window: Window? = null): Bitmap {
|
||||
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||
error("toBitmap() can't be called from the main thread")
|
||||
}
|
||||
|
||||
if (!HardwareRenderer.isDrawingEnabled()) {
|
||||
error("Hardware rendering is not enabled")
|
||||
}
|
||||
|
||||
// Make sure we are idle.
|
||||
Espresso.onIdle()
|
||||
|
||||
val mainExecutor = context.mainExecutor
|
||||
return runBlocking {
|
||||
suspendCoroutine { continuation ->
|
||||
Futures.addCallback(
|
||||
captureToBitmap(window),
|
||||
object : FutureCallback<Bitmap> {
|
||||
override fun onSuccess(result: Bitmap?) {
|
||||
continuation.resumeWith(Result.success(result!!))
|
||||
}
|
||||
|
||||
override fun onFailure(t: Throwable) {
|
||||
continuation.resumeWith(Result.failure(t))
|
||||
}
|
||||
},
|
||||
// We know that we are not on the main thread, so we can block the current
|
||||
// thread and wait for the result in the main thread.
|
||||
mainExecutor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a redraw of the given view.
|
||||
*
|
||||
* Should only be called on UI thread.
|
||||
*
|
||||
* @return a [ListenableFuture] that will be complete once ui drawing is complete
|
||||
*/
|
||||
// NoClassDefFoundError occurs on API 15
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
// @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||
@ExperimentalTestApi
|
||||
fun View.forceRedraw(): ListenableFuture<Void> {
|
||||
val future: ResolvableFuture<Void> = ResolvableFuture.create()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 29 && isHardwareAccelerated) {
|
||||
viewTreeObserver.registerFrameCommitCallback() { future.set(null) }
|
||||
} else {
|
||||
viewTreeObserver.addOnDrawListener(
|
||||
object : ViewTreeObserver.OnDrawListener {
|
||||
var handled = false
|
||||
override fun onDraw() {
|
||||
if (!handled) {
|
||||
handled = true
|
||||
future.set(null)
|
||||
// cannot remove on draw listener inside of onDraw
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
viewTreeObserver.removeOnDrawListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
invalidate()
|
||||
return future
|
||||
}
|
||||
|
||||
private fun View.generateBitmap(
|
||||
bitmapFuture: ResolvableFuture<Bitmap>,
|
||||
window: Window? = null,
|
||||
) {
|
||||
if (bitmapFuture.isCancelled) {
|
||||
return
|
||||
}
|
||||
val destBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
when {
|
||||
Build.VERSION.SDK_INT < 26 -> generateBitmapFromDraw(destBitmap, bitmapFuture)
|
||||
this is SurfaceView -> generateBitmapFromSurfaceViewPixelCopy(destBitmap, bitmapFuture)
|
||||
else -> {
|
||||
val window = window ?: getActivity()?.window
|
||||
if (window != null) {
|
||||
generateBitmapFromPixelCopy(window, destBitmap, bitmapFuture)
|
||||
} else {
|
||||
Log.i(
|
||||
"View.captureToImage",
|
||||
"Could not find window for view. Falling back to View#draw instead of PixelCopy"
|
||||
)
|
||||
generateBitmapFromDraw(destBitmap, bitmapFuture)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("NewApi")
|
||||
private fun SurfaceView.generateBitmapFromSurfaceViewPixelCopy(
|
||||
destBitmap: Bitmap,
|
||||
bitmapFuture: ResolvableFuture<Bitmap>
|
||||
) {
|
||||
val onCopyFinished =
|
||||
PixelCopy.OnPixelCopyFinishedListener { result ->
|
||||
if (result == PixelCopy.SUCCESS) {
|
||||
bitmapFuture.set(destBitmap)
|
||||
} else {
|
||||
bitmapFuture.setException(
|
||||
RuntimeException(String.format("PixelCopy failed: %d", result))
|
||||
)
|
||||
}
|
||||
}
|
||||
PixelCopy.request(this, null, destBitmap, onCopyFinished, handler)
|
||||
}
|
||||
|
||||
internal fun View.generateBitmapFromDraw(
|
||||
destBitmap: Bitmap,
|
||||
bitmapFuture: ResolvableFuture<Bitmap>
|
||||
) {
|
||||
destBitmap.density = resources.displayMetrics.densityDpi
|
||||
computeScroll()
|
||||
val canvas = Canvas(destBitmap)
|
||||
canvas.translate((-scrollX).toFloat(), (-scrollY).toFloat())
|
||||
draw(canvas)
|
||||
bitmapFuture.set(destBitmap)
|
||||
}
|
||||
|
||||
private fun View.getActivity(): Activity? {
|
||||
fun Context.getActivity(): Activity? {
|
||||
return when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> this.baseContext.getActivity()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
return context.getActivity()
|
||||
}
|
||||
|
||||
private fun View.generateBitmapFromPixelCopy(
|
||||
window: Window,
|
||||
destBitmap: Bitmap,
|
||||
bitmapFuture: ResolvableFuture<Bitmap>
|
||||
) {
|
||||
val locationInWindow = intArrayOf(0, 0)
|
||||
getLocationInWindow(locationInWindow)
|
||||
val x = locationInWindow[0]
|
||||
val y = locationInWindow[1]
|
||||
val boundsInWindow = Rect(x, y, x + width, y + height)
|
||||
|
||||
return window.generateBitmapFromPixelCopy(boundsInWindow, destBitmap, bitmapFuture)
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.systemui.testing.screenshot
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import platform.test.screenshot.DeviceEmulationRule
|
||||
import platform.test.screenshot.DeviceEmulationSpec
|
||||
import platform.test.screenshot.MaterialYouColorsRule
|
||||
import platform.test.screenshot.ScreenshotTestRule
|
||||
import platform.test.screenshot.getEmulatedDevicePathConfig
|
||||
import platform.test.screenshot.matchers.BitmapMatcher
|
||||
|
||||
/** A rule for View screenshot diff unit tests. */
|
||||
open class ViewScreenshotTestRule(
|
||||
emulationSpec: DeviceEmulationSpec,
|
||||
private val matcher: BitmapMatcher = UnitTestBitmapMatcher,
|
||||
assetsPathRelativeToBuildRoot: String
|
||||
) : TestRule {
|
||||
private val colorsRule = MaterialYouColorsRule()
|
||||
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
|
||||
protected val screenshotRule =
|
||||
ScreenshotTestRule(
|
||||
SystemUIGoldenImagePathManager(
|
||||
getEmulatedDevicePathConfig(emulationSpec),
|
||||
assetsPathRelativeToBuildRoot
|
||||
)
|
||||
)
|
||||
private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
|
||||
private val roboRule =
|
||||
RuleChain.outerRule(deviceEmulationRule).around(screenshotRule).around(activityRule)
|
||||
private val delegateRule = RuleChain.outerRule(colorsRule).around(roboRule)
|
||||
private val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
if (isRobolectric) {
|
||||
// In robolectric mode, we enable NATIVE graphics and unpack font and icu files.
|
||||
// We need to use reflection, as this library is only needed and therefore
|
||||
// only available in deviceless mode.
|
||||
val nativeLoaderClassName = "org.robolectric.nativeruntime.DefaultNativeRuntimeLoader"
|
||||
val defaultNativeRuntimeLoader = Class.forName(nativeLoaderClassName)
|
||||
System.setProperty("robolectric.graphicsMode", "NATIVE")
|
||||
defaultNativeRuntimeLoader.getMethod("injectAndLoad").invoke(null)
|
||||
}
|
||||
val ruleToApply = if (isRobolectric) roboRule else delegateRule
|
||||
return ruleToApply.apply(base, description)
|
||||
}
|
||||
|
||||
protected fun takeScreenshot(
|
||||
mode: Mode = Mode.WrapContent,
|
||||
viewProvider: (ComponentActivity) -> View,
|
||||
beforeScreenshot: (ComponentActivity) -> Unit = {}
|
||||
): Bitmap {
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
// Make sure that the activity draws full screen and fits the whole display instead of
|
||||
// the system bars.
|
||||
val window = activity.window
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
|
||||
// Set the content.
|
||||
activity.setContentView(viewProvider(activity), mode.layoutParams)
|
||||
|
||||
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable
|
||||
// it for any view in the hierarchy.
|
||||
window.decorView.removeElevationRecursively()
|
||||
|
||||
activity.currentFocus?.clearFocus()
|
||||
}
|
||||
|
||||
// We call onActivity again because it will make sure that our Activity is done measuring,
|
||||
// laying out and drawing its content (that we set in the previous onActivity lambda).
|
||||
var contentView: View? = null
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
// Check that the content is what we expected.
|
||||
val content = activity.requireViewById<ViewGroup>(android.R.id.content)
|
||||
assertEquals(1, content.childCount)
|
||||
contentView = content.getChildAt(0)
|
||||
beforeScreenshot(activity)
|
||||
}
|
||||
|
||||
return if (isRobolectric) {
|
||||
contentView?.captureToBitmap()?.get(10, TimeUnit.SECONDS)
|
||||
?: error("timeout while trying to capture view to bitmap")
|
||||
} else {
|
||||
contentView?.toBitmap() ?: error("contentView is null")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the content of the view provided by [viewProvider] with the golden image identified
|
||||
* by [goldenIdentifier] in the context of [emulationSpec].
|
||||
*/
|
||||
fun screenshotTest(
|
||||
goldenIdentifier: String,
|
||||
mode: Mode = Mode.WrapContent,
|
||||
beforeScreenshot: (ComponentActivity) -> Unit = {},
|
||||
viewProvider: (ComponentActivity) -> View
|
||||
) {
|
||||
val bitmap = takeScreenshot(mode, viewProvider, beforeScreenshot)
|
||||
screenshotRule.assertBitmapAgainstGolden(
|
||||
bitmap,
|
||||
goldenIdentifier,
|
||||
matcher,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the content of the dialog provided by [dialogProvider] with the golden image
|
||||
* identified by [goldenIdentifier] in the context of [emulationSpec].
|
||||
*/
|
||||
fun dialogScreenshotTest(
|
||||
goldenIdentifier: String,
|
||||
dialogProvider: (Activity) -> Dialog,
|
||||
) {
|
||||
var dialog: Dialog? = null
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
dialog =
|
||||
dialogProvider(activity).apply {
|
||||
// Make sure that the dialog draws full screen and fits the whole display
|
||||
// instead of the system bars.
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
|
||||
// Disable enter/exit animations.
|
||||
create()
|
||||
window.setWindowAnimations(0)
|
||||
|
||||
// Elevation/shadows is not deterministic when doing hardware rendering, so we
|
||||
// disable it for any view in the hierarchy.
|
||||
window.decorView.removeElevationRecursively()
|
||||
|
||||
// Show the dialog.
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val bitmap = dialog?.toBitmap() ?: error("dialog is null")
|
||||
screenshotRule.assertBitmapAgainstGolden(
|
||||
bitmap,
|
||||
goldenIdentifier,
|
||||
matcher,
|
||||
)
|
||||
} finally {
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Dialog.toBitmap(): Bitmap {
|
||||
val window = window
|
||||
return window.decorView.toBitmap(window)
|
||||
}
|
||||
|
||||
enum class Mode(val layoutParams: LayoutParams) {
|
||||
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
|
||||
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
|
||||
MatchWidth(LayoutParams(MATCH_PARENT, WRAP_CONTENT)),
|
||||
MatchHeight(LayoutParams(WRAP_CONTENT, MATCH_PARENT)),
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.android.systemui.testing.screenshot
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.PixelCopy
|
||||
import android.view.Window
|
||||
import androidx.concurrent.futures.ResolvableFuture
|
||||
|
||||
/*
|
||||
* This file was forked from androidx/test/core/view/WindowCapture.kt.
|
||||
* TODO(b/195673633): Remove this fork and use the AndroidX version instead.
|
||||
*/
|
||||
fun Window.generateBitmapFromPixelCopy(
|
||||
boundsInWindow: Rect? = null,
|
||||
destBitmap: Bitmap,
|
||||
bitmapFuture: ResolvableFuture<Bitmap>
|
||||
) {
|
||||
val onCopyFinished =
|
||||
PixelCopy.OnPixelCopyFinishedListener { result ->
|
||||
if (result == PixelCopy.SUCCESS) {
|
||||
bitmapFuture.set(destBitmap)
|
||||
} else {
|
||||
bitmapFuture.setException(
|
||||
RuntimeException(String.format("PixelCopy failed: %d", result))
|
||||
)
|
||||
}
|
||||
}
|
||||
PixelCopy.request(
|
||||
this,
|
||||
boundsInWindow,
|
||||
destBitmap,
|
||||
onCopyFinished,
|
||||
Handler(Looper.getMainLooper())
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user