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.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimatedVectorDrawable;
@@ -146,6 +147,7 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
@@ -281,6 +283,7 @@ public class ChooserActivity extends ResolverActivity implements
private long mQueriedSharingShortcutsTimeMs; private long mQueriedSharingShortcutsTimeMs;
private int mCurrAvailableWidth = 0; private int mCurrAvailableWidth = 0;
private Insets mLastAppliedInsets = null;
private int mLastNumberOfChildren = -1; private int mLastNumberOfChildren = -1;
private int mMaxTargetsPerRow = 1; private int mMaxTargetsPerRow = 1;
@@ -2546,7 +2549,11 @@ public class ChooserActivity extends ResolverActivity implements
|| gridAdapter.calculateChooserTargetWidth(availableWidth) || gridAdapter.calculateChooserTargetWidth(availableWidth)
|| recyclerView.getAdapter() == null || recyclerView.getAdapter() == null
|| availableWidth != mCurrAvailableWidth; || availableWidth != mCurrAvailableWidth;
boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
if (isLayoutUpdated if (isLayoutUpdated
|| insetsChanged
|| mLastNumberOfChildren != recyclerView.getChildCount()) { || mLastNumberOfChildren != recyclerView.getChildCount()) {
mCurrAvailableWidth = availableWidth; mCurrAvailableWidth = availableWidth;
if (isLayoutUpdated) { if (isLayoutUpdated) {
@@ -2567,7 +2574,7 @@ public class ChooserActivity extends ResolverActivity implements
return; return;
} }
if (mLastNumberOfChildren == recyclerView.getChildCount()) { if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
return; return;
} }
@@ -2578,6 +2585,7 @@ public class ChooserActivity extends ResolverActivity implements
int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter); int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
mResolverDrawerLayout.setCollapsibleHeightReserved(offset); mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
mEnterTransitionAnimationDelegate.markOffsetCalculated(); mEnterTransitionAnimationDelegate.markOffsetCalculated();
mLastAppliedInsets = mSystemWindowInsets;
}); });
} }
} }
@@ -3070,7 +3078,12 @@ public class ChooserActivity extends ResolverActivity implements
mChooserMultiProfilePagerAdapter.setupContainerPadding( mChooserMultiProfilePagerAdapter.setupContainerPadding(
getActiveEmptyStateView().findViewById(R.id.resolver_empty_state_container)); 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) { 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 package com.android.systemui.testing.screenshot
import android.annotation.WorkerThread
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.HardwareRenderer
import android.graphics.Rect import android.graphics.Rect
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
@@ -19,8 +21,13 @@ import androidx.annotation.RequiresApi
import androidx.concurrent.futures.ResolvableFuture import androidx.concurrent.futures.ResolvableFuture
import androidx.test.annotation.ExperimentalTestApi import androidx.test.annotation.ExperimentalTestApi
import androidx.test.core.internal.os.HandlerExecutor import androidx.test.core.internal.os.HandlerExecutor
import androidx.test.espresso.Espresso
import androidx.test.platform.graphics.HardwareRendererCompat 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 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 * 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 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. * 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.Activity
import android.app.Dialog import android.app.Dialog
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.HardwareRenderer
import android.os.Looper
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.Window
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.test.espresso.Espresso
import androidx.test.ext.junit.rules.ActivityScenarioRule 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.Assert.assertEquals
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.rules.TestRule import org.junit.rules.TestRule
@@ -89,6 +81,8 @@ class ViewScreenshotTestRule(
// Elevation/shadows is not deterministic when doing hardware rendering, so we disable // Elevation/shadows is not deterministic when doing hardware rendering, so we disable
// it for any view in the hierarchy. // it for any view in the hierarchy.
window.decorView.removeElevationRecursively() window.decorView.removeElevationRecursively()
activity.currentFocus?.clearFocus()
} }
// We call onActivity again because it will make sure that our Activity is done measuring, // 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 { private fun Dialog.toBitmap(): Bitmap {
val window = window val window = window
return window.decorView.toBitmap(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) { enum class Mode(val layoutParams: LayoutParams) {
WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)), WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)), MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),