Merge "Migrating code from systemui to common library." into udc-qpr-dev

This commit is contained in:
Ram Peri
2023-07-07 02:30:24 +00:00
committed by Android (Google) Code Review
15 changed files with 0 additions and 1065 deletions

View File

@@ -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"],
}

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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"],
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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?"
)
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)),
}
}

View File

@@ -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())
)
}