Merge changes from topic "pss-app-selector-screenshot" into tm-qpr-dev am: 47c21d1216

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19536771

Change-Id: I3abe0699529a84f05e57bee59eacf2cc60a4dc22
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Nick Chameyev
2022-09-06 13:31:52 +00:00
committed by Automerger Merge Worker
6 changed files with 266 additions and 52 deletions

View File

@@ -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;
@@ -2546,7 +2549,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) {
@@ -2567,7 +2574,7 @@ public class ChooserActivity extends ResolverActivity implements
return;
}
if (mLastNumberOfChildren == recyclerView.getChildCount()) {
if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
return;
}
@@ -2578,6 +2585,7 @@ public class ChooserActivity extends ResolverActivity implements
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
mEnterTransitionAnimationDelegate.markOffsetCalculated();
mLastAppliedInsets = mSystemWindowInsets;
});
}
}
@@ -3070,7 +3078,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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -89,6 +81,8 @@ class ViewScreenshotTestRule(
// 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,
@@ -150,53 +144,11 @@ class ViewScreenshotTestRule(
}
}
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)),