Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ea0153c0 authored by Nick Chameyev's avatar Nick Chameyev Committed by Automerger Merge Worker
Browse files

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

parents 21ceb390 47c21d12
Loading
Loading
Loading
Loading
+15 −2
Original line number Diff line number Diff line
@@ -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) {
+99 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+60 −0
Original line number Diff line number Diff line
/*
 * 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?"
                )
    }
}
+42 −0
Original line number Diff line number Diff line
/*
 * 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)
}
+48 −0
Original line number Diff line number Diff line
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.
 *
Loading