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