[Partial Screensharing] Add app selector activity screenshot test
Adds a screenshot test for the app selector activity. It also contains changes to utility rules and classes to allow capturing the whole activity instead of a single view. Bug: 235465652 Test: atest com.android.systemui.mediaprojection.MediaProjectionAppSelectorScreenshotTest Change-Id: I558c3d0e8893091d42f90cb0358c9fd030cbc0dc
This commit is contained in:
@@ -63,6 +63,7 @@ import android.database.DataSetObserver;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
@@ -146,6 +147,7 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@@ -281,6 +283,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
private long mQueriedSharingShortcutsTimeMs;
|
||||
|
||||
private int mCurrAvailableWidth = 0;
|
||||
private Insets mLastAppliedInsets = null;
|
||||
private int mLastNumberOfChildren = -1;
|
||||
private int mMaxTargetsPerRow = 1;
|
||||
|
||||
@@ -2533,7 +2536,11 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
|| gridAdapter.calculateChooserTargetWidth(availableWidth)
|
||||
|| recyclerView.getAdapter() == null
|
||||
|| availableWidth != mCurrAvailableWidth;
|
||||
|
||||
boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
|
||||
|
||||
if (isLayoutUpdated
|
||||
|| insetsChanged
|
||||
|| mLastNumberOfChildren != recyclerView.getChildCount()) {
|
||||
mCurrAvailableWidth = availableWidth;
|
||||
if (isLayoutUpdated) {
|
||||
@@ -2554,7 +2561,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
return;
|
||||
}
|
||||
|
||||
if (mLastNumberOfChildren == recyclerView.getChildCount()) {
|
||||
if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2565,6 +2572,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
|
||||
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
|
||||
mEnterTransitionAnimationDelegate.markOffsetCalculated();
|
||||
mLastAppliedInsets = mSystemWindowInsets;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3057,7 +3065,12 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
mChooserMultiProfilePagerAdapter.setupContainerPadding(
|
||||
getActiveEmptyStateView().findViewById(R.id.resolver_empty_state_container));
|
||||
}
|
||||
return super.onApplyWindowInsets(v, insets);
|
||||
|
||||
WindowInsets result = super.onApplyWindowInsets(v, insets);
|
||||
if (mResolverDrawerLayout != null) {
|
||||
mResolverDrawerLayout.requestLayout();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void setHorizontalScrollingEnabled(boolean enabled) {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.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) : TestRule {
|
||||
|
||||
private val colorsRule = MaterialYouColorsRule()
|
||||
private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
|
||||
private val screenshotRule =
|
||||
ScreenshotTestRule(
|
||||
SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec))
|
||||
)
|
||||
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].
|
||||
*/
|
||||
fun screenshotTest(goldenIdentifier: String, view: View) {
|
||||
view.removeElevationRecursively()
|
||||
|
||||
ScreenshotRuleAsserter.Builder(screenshotRule)
|
||||
.setScreenshotProvider { view.toBitmap() }
|
||||
.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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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?"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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,10 +1,12 @@
|
||||
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
|
||||
@@ -19,8 +21,13 @@ 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
|
||||
@@ -61,6 +68,47 @@ fun View.captureToBitmap(window: Window? = null): ListenableFuture<Bitmap> {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -19,21 +19,13 @@ package com.android.systemui.testing.screenshot
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.HardwareRenderer
|
||||
import android.os.Looper
|
||||
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 android.view.Window
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
@@ -86,6 +78,8 @@ class ViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
|
||||
// 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,
|
||||
@@ -147,53 +141,11 @@ class ViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.removeElevationRecursively() {
|
||||
this.elevation = 0f
|
||||
|
||||
if (this is ViewGroup) {
|
||||
repeat(childCount) { i -> getChildAt(i).removeElevationRecursively() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Dialog.toBitmap(): Bitmap {
|
||||
val window = window
|
||||
return window.decorView.toBitmap(window)
|
||||
}
|
||||
|
||||
private 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Mode(val layoutParams: LayoutParams) {
|
||||
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
|
||||
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
|
||||
|
||||
Reference in New Issue
Block a user