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

Commit 52ff494c authored by Massimo Carli's avatar Massimo Carli
Browse files

[2/n] TransparentPolicyState definition

Incapsulate the state the transparent activity inherits from the
opaque one, into a TransparentPolicyState object.

The TransparentPolicy for a given ActivityRecord is running if
it has a valid TransparentPolicyState.

Flag: EXEMPT refactor
Bug: 337346942
Test: atest WmTests:SizeCompatTests
Test: atest WmTests:LetterboxUiControllerTest

Change-Id: I60d9387f4f50246dc61a4168d45f559a43e66877
parent 0b0860b4
Loading
Loading
Loading
Loading
+25 −5
Original line number Diff line number Diff line
@@ -132,6 +132,7 @@ import java.io.PrintWriter;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Predicate;

/** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
// TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
@@ -244,7 +245,24 @@ final class LetterboxUiController {
        // to use it after since controller is only used in ActivityRecord.
        mActivityRecord = activityRecord;

        mTransparentPolicy = new TransparentPolicy(activityRecord, mLetterboxConfiguration);
        mTransparentPolicy = new TransparentPolicy(activityRecord, mLetterboxConfiguration,
                new Predicate<ActivityRecord>() {
                    @Override
                    public boolean test(ActivityRecord opaqueActivity) {
                        if (opaqueActivity == null || opaqueActivity.isEmbedded()) {
                            // We skip letterboxing if the translucent activity doesn't have any
                            // opaque activities beneath or the activity below is embedded which
                            // never has letterbox.
                            mActivityRecord.recomputeConfiguration();
                            return true;
                        }
                        if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
                                || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
                            return true;
                        }
                        return false;
                    }
                });
        PackageManager packageManager = wmService.mContext.getPackageManager();

        final OptPropFactory optPropBuilder = new OptPropFactory(packageManager,
@@ -322,7 +340,7 @@ final class LetterboxUiController {
            mLetterbox.destroy();
            mLetterbox = null;
        }
        mTransparentPolicy.destroy();
        mTransparentPolicy.stop();
    }

    void onMovedToDisplay(int displayId) {
@@ -1303,7 +1321,8 @@ final class LetterboxUiController {
        // Use screen resolved bounds which uses resolved bounds or size compat bounds
        // as activity bounds can sometimes be empty
        final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
                ? getTransparentPolicy().mFirstOpaqueActivityBeneath.getScreenResolvedBounds()
                ? getTransparentPolicy().getTransparentPolicyState()
                    .mFirstOpaqueActivity.getScreenResolvedBounds()
                : mActivityRecord.getScreenResolvedBounds();
        return mLetterboxConfiguration.getIsHorizontalReachabilityEnabled()
                && parentConfiguration.windowConfiguration.getWindowingMode()
@@ -1341,7 +1360,8 @@ final class LetterboxUiController {
        // Use screen resolved bounds which uses resolved bounds or size compat bounds
        // as activity bounds can sometimes be empty
        final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
                ? getTransparentPolicy().mFirstOpaqueActivityBeneath.getScreenResolvedBounds()
                ? getTransparentPolicy().getTransparentPolicyState()
                    .mFirstOpaqueActivity.getScreenResolvedBounds()
                : mActivityRecord.getScreenResolvedBounds();
        return mLetterboxConfiguration.getIsVerticalReachabilityEnabled()
                && parentConfiguration.windowConfiguration.getWindowingMode()
@@ -1758,7 +1778,7 @@ final class LetterboxUiController {
     * first opaque activity beneath.
     */
    void updateInheritedLetterbox() {
        mTransparentPolicy.updateInheritedLetterbox();
        mTransparentPolicy.start();
    }

    /**
+189 −119
Original line number Diff line number Diff line
@@ -31,11 +31,10 @@ import android.annotation.Nullable;
import android.content.res.Configuration;
import android.graphics.Rect;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Predicate;

@@ -46,126 +45,75 @@ class TransparentPolicy {

    private static final String TAG = TAG_WITH_CLASS_NAME ? "TransparentPolicy" : TAG_ATM;

    // Aspect ratio value to consider as undefined.
    private static final float UNDEFINED_ASPECT_RATIO = 0f;

    // The predicate used to find the first opaque not finishing activity below the potential
    // transparent activity.
    private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
            ActivityRecord::occludesParent;

    // The predicate to check to skip the policy
    @NonNull
    private final Predicate<ActivityRecord> mSkipLetterboxPredicate;

    // The ActivityRecord this policy relates to.
    private final ActivityRecord mActivityRecord;

    // The LetterboxConfiguration
    private final LetterboxConfiguration mLetterboxConfiguration;
    // If transparent activity policy is enabled.
    private final BooleanSupplier mIsTranslucentLetterboxingEnabledSupplier;

    // The list of observers for the destroy event of candidate opaque activities
    // when dealing with translucent activities.
    private final List<TransparentPolicy> mDestroyListeners = new ArrayList<>();

    // In case of transparent activities we might need to access the aspectRatio of the
    // first opaque activity beneath.
    private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
    private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;

    @Configuration.Orientation
    private int mInheritedOrientation = ORIENTATION_UNDEFINED;

    // The app compat state for the opaque activity if any
    private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;

    // The CompatDisplayInsets of the opaque activity beneath the translucent one.
    private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;

    /*
     * WindowContainerListener responsible to make translucent activities inherit
     * constraints from the first opaque activity beneath them. It's null for not
     * translucent activities.
     */
    @Nullable
    private WindowContainerListener mLetterboxConfigListener;


    @Nullable
    @VisibleForTesting
    ActivityRecord mFirstOpaqueActivityBeneath;
    // THe current state for the possible transparent activity
    private final TransparentPolicyState mTransparentPolicyState;

    TransparentPolicy(@NonNull ActivityRecord activityRecord,
            @NonNull LetterboxConfiguration letterboxConfiguration) {
            @NonNull LetterboxConfiguration letterboxConfiguration,
            @NonNull Predicate<ActivityRecord> skipLetterboxPredicate) {
        mActivityRecord = activityRecord;
        mLetterboxConfiguration = letterboxConfiguration;
        mIsTranslucentLetterboxingEnabledSupplier = () -> letterboxConfiguration
                .isTranslucentLetterboxingEnabled();
        mSkipLetterboxPredicate = skipLetterboxPredicate;
        mTransparentPolicyState = new TransparentPolicyState(activityRecord);
    }

    /**
     * Handles translucent activities letterboxing inheriting constraints from the
     * first opaque activity beneath.
     */
    void updateInheritedLetterbox() {
        final WindowContainer<?> parent = mActivityRecord.getParent();
        if (parent == null) {
    void start() {
        if (!mIsTranslucentLetterboxingEnabledSupplier.getAsBoolean()) {
            return;
        }
        if (!mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
        final WindowContainer<?> parent = mActivityRecord.getParent();
        if (parent == null) {
            return;
        }
        if (mLetterboxConfigListener != null) {
            mLetterboxConfigListener.onRemoved();
            clearInheritedConfig();
        }
        mTransparentPolicyState.reset();
        // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the
        // opaque activity constraints because we're expecting the activity is already letterboxed.
        mFirstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity(
        final ActivityRecord firstOpaqueActivity = mActivityRecord.getTask().getActivity(
                FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
                mActivityRecord /* boundary */, false /* includeBoundary */,
                true /* traverseTopToBottom */);
        if (mFirstOpaqueActivityBeneath == null || mFirstOpaqueActivityBeneath.isEmbedded()) {
            // We skip letterboxing if the translucent activity doesn't have any opaque
            // activities beneath or the activity below is embedded which never has letterbox.
            mActivityRecord.recomputeConfiguration();
        // We check if we need for some reason to skip the policy gievn the specific first
        // opaque activity
        if (mSkipLetterboxPredicate.test(firstOpaqueActivity)) {
            return;
        }
        if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
                || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
            return;
        mTransparentPolicyState.start(firstOpaqueActivity);
    }
        mFirstOpaqueActivityBeneath.mLetterboxUiController.getTransparentPolicy()
                .mDestroyListeners.add(this);
        inheritConfiguration(mFirstOpaqueActivityBeneath);
        mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation(
                mActivityRecord, mFirstOpaqueActivityBeneath,
                (opaqueConfig, transparentOverrideConfig) -> {
                    resetTranslucentOverrideConfig(transparentOverrideConfig);
                    final Rect parentBounds = parent.getWindowConfiguration().getBounds();
                    final Rect bounds = transparentOverrideConfig.windowConfiguration.getBounds();
                    final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds();
                    // We cannot use letterboxBounds directly here because the position relies on
                    // letterboxing. Using letterboxBounds directly, would produce a double offset.
                    bounds.set(parentBounds.left, parentBounds.top,
                            parentBounds.left + letterboxBounds.width(),
                            parentBounds.top + letterboxBounds.height());
                    // We need to initialize appBounds to avoid NPE. The actual value will
                    // be set ahead when resolving the Configuration for the activity.
                    transparentOverrideConfig.windowConfiguration.setAppBounds(new Rect());
                    inheritConfiguration(mFirstOpaqueActivityBeneath);
                    return transparentOverrideConfig;
                });

    }

    void destroy() {
    void stop() {
        for (int i = mDestroyListeners.size() - 1; i >= 0; i--) {
            mDestroyListeners.get(i).updateInheritedLetterbox();
            mDestroyListeners.get(i).start();
        }
        mDestroyListeners.clear();
        if (mLetterboxConfigListener != null) {
            mLetterboxConfigListener.onRemoved();
            mLetterboxConfigListener = null;
        }
        mTransparentPolicyState.reset();
    }

    boolean hasInheritedLetterboxBehavior() {
        return mLetterboxConfigListener != null;
        return mTransparentPolicyState.isRunning();
    }

    /**
@@ -184,28 +132,32 @@ class TransparentPolicy {
    }

    float getInheritedMinAspectRatio() {
        return mInheritedMinAspectRatio;
        return mTransparentPolicyState.getInheritedMinAspectRatio();
    }

    float getInheritedMaxAspectRatio() {
        return mInheritedMaxAspectRatio;
        return mTransparentPolicyState.getInheritedMaxAspectRatio();
    }

    int getInheritedAppCompatState() {
        return mInheritedAppCompatState;
        return mTransparentPolicyState.getInheritedAppCompatState();
    }

    @Configuration.Orientation
    int getInheritedOrientation() {
        return mInheritedOrientation;
        return mTransparentPolicyState.getInheritedOrientation();
    }

    ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
        return mInheritedCompatDisplayInsets;
        return mTransparentPolicyState.getInheritedCompatDisplayInsets();
    }

    void clearInheritedCompatDisplayInsets() {
        mInheritedCompatDisplayInsets = null;
        mTransparentPolicyState.clearInheritedCompatDisplayInsets();
    }

    TransparentPolicyState getTransparentPolicyState() {
        return mTransparentPolicyState;
    }

    /**
@@ -213,11 +165,7 @@ class TransparentPolicy {
     * activity beneath using the given consumer and returns {@code true}.
     */
    boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
        return findOpaqueNotFinishingActivityBelow()
                .map(activityRecord -> {
                    consumer.accept(activityRecord);
                    return true;
                }).orElse(false);
        return mTransparentPolicyState.applyOnOpaqueActivityBelow(consumer);
    }

    /**
@@ -225,10 +173,7 @@ class TransparentPolicy {
     * if it exists and the strategy is enabled.
     */
    Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
        if (!hasInheritedLetterboxBehavior() || mActivityRecord.getTask() == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(mFirstOpaqueActivityBeneath);
        return mTransparentPolicyState.findOpaqueNotFinishingActivityBelow();
    }

    /** Resets the screen size related fields so they can be resolved by requested bounds later. */
@@ -244,33 +189,158 @@ class TransparentPolicy {
    }

    private void inheritConfiguration(ActivityRecord firstOpaque) {
        mTransparentPolicyState.inheritFromOpaque(firstOpaque);
    }

    /**
     * Encapsulate the state for the current translucent activity when the transparent policy
     * has started.
     */
    static class TransparentPolicyState {
        // Aspect ratio value to consider as undefined.
        private static final float UNDEFINED_ASPECT_RATIO = 0f;

        @NonNull
        private final ActivityRecord mActivityRecord;

        @Configuration.Orientation
        private int mInheritedOrientation = ORIENTATION_UNDEFINED;
        private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
        private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;

        // The app compat state for the opaque activity if any
        private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;

        // The CompatDisplayInsets of the opaque activity beneath the translucent one.
        private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;

        @Nullable
        ActivityRecord mFirstOpaqueActivity;

        /*
         * WindowContainerListener responsible to make translucent activities inherit
         * constraints from the first opaque activity beneath them. It's null for not
         * translucent activities.
         */
        @Nullable
        private WindowContainerListener mLetterboxConfigListener;

        TransparentPolicyState(@NonNull ActivityRecord activityRecord) {
            mActivityRecord = activityRecord;
        }

        private void start(@NonNull ActivityRecord firstOpaqueActivity) {
            mFirstOpaqueActivity = firstOpaqueActivity;
            mFirstOpaqueActivity.mLetterboxUiController.getTransparentPolicy()
                    .mDestroyListeners.add(mActivityRecord.mLetterboxUiController
                            .getTransparentPolicy());
            inheritFromOpaque(firstOpaqueActivity);
            final WindowContainer<?> parent = mActivityRecord.getParent();
            mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation(
                    mActivityRecord, mFirstOpaqueActivity,
                    (opaqueConfig, transparentOverrideConfig) -> {
                        resetTranslucentOverrideConfig(transparentOverrideConfig);
                        final Rect parentBounds = parent.getWindowConfiguration().getBounds();
                        final Rect bounds = transparentOverrideConfig
                                .windowConfiguration.getBounds();
                        final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds();
                        // We cannot use letterboxBounds directly here because the position relies
                        // on letterboxing. Using letterboxBounds directly, would produce a
                        // double offset.
                        bounds.set(parentBounds.left, parentBounds.top,
                                parentBounds.left + letterboxBounds.width(),
                                parentBounds.top + letterboxBounds.height());
                        // We need to initialize appBounds to avoid NPE. The actual value will
                        // be set ahead when resolving the Configuration for the activity.
                        transparentOverrideConfig.windowConfiguration.setAppBounds(new Rect());
                        inheritFromOpaque(mFirstOpaqueActivity);
                        return transparentOverrideConfig;
                    });
        }

        private void inheritFromOpaque(@NonNull ActivityRecord opaqueActivity) {
            // To avoid wrong behaviour, we're not forcing a specific aspect ratio to activities
            // which are not already providing one (e.g. permission dialogs) and presumably also
            // not resizable.
            if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) {
            mInheritedMinAspectRatio = firstOpaque.getMinAspectRatio();
                mInheritedMinAspectRatio = opaqueActivity.getMinAspectRatio();
            }
            if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) {
            mInheritedMaxAspectRatio = firstOpaque.getMaxAspectRatio();
                mInheritedMaxAspectRatio = opaqueActivity.getMaxAspectRatio();
            }
        mInheritedOrientation = firstOpaque.getRequestedConfigurationOrientation();
        mInheritedAppCompatState = firstOpaque.getAppCompatState();
        mInheritedCompatDisplayInsets = firstOpaque.getCompatDisplayInsets();
            mInheritedOrientation = opaqueActivity.getRequestedConfigurationOrientation();
            mInheritedAppCompatState = opaqueActivity.getAppCompatState();
            mInheritedCompatDisplayInsets = opaqueActivity.getCompatDisplayInsets();
        }

    private void clearInheritedConfig() {
        if (mFirstOpaqueActivityBeneath != null) {
            mFirstOpaqueActivityBeneath.mLetterboxUiController.getTransparentPolicy()
                    .mDestroyListeners.remove(this);
        private void reset() {
            if (mLetterboxConfigListener != null) {
                mLetterboxConfigListener.onRemoved();
            }
        mFirstOpaqueActivityBeneath = null;
            mLetterboxConfigListener = null;
            mInheritedOrientation = ORIENTATION_UNDEFINED;
            mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
            mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
        mInheritedOrientation = ORIENTATION_UNDEFINED;
            mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
            mInheritedCompatDisplayInsets = null;
            if (mFirstOpaqueActivity != null) {
                mFirstOpaqueActivity.mLetterboxUiController.getTransparentPolicy()
                        .mDestroyListeners.remove(mActivityRecord.mLetterboxUiController
                                .getTransparentPolicy());
            }
            mFirstOpaqueActivity = null;
        }

        private boolean isRunning() {
            return mLetterboxConfigListener != null;
        }

        private int getInheritedOrientation() {
            return mInheritedOrientation;
        }

        private float getInheritedMinAspectRatio() {
            return mInheritedMinAspectRatio;
        }

        private float getInheritedMaxAspectRatio() {
            return mInheritedMaxAspectRatio;
        }

        private int getInheritedAppCompatState() {
            return mInheritedAppCompatState;
        }

        private ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
            return mInheritedCompatDisplayInsets;
        }

        private void clearInheritedCompatDisplayInsets() {
            mInheritedCompatDisplayInsets = null;
        }

        /**
         * @return The first not finishing opaque activity beneath the current translucent activity
         * if it exists and the strategy is enabled.
         */
        private Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
            if (!isRunning() || mActivityRecord.getTask() == null) {
                return Optional.empty();
            }
            return Optional.ofNullable(mFirstOpaqueActivity);
        }

        /**
         * In case of translucent activities, it consumes the {@link ActivityRecord} of the first
         * opaque activity beneath using the given consumer and returns {@code true}.
         */
        private boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
            return findOpaqueNotFinishingActivityBelow()
                    .map(activityRecord -> {
                        consumer.accept(activityRecord);
                        return true;
                    }).orElse(false);
        }
    }

}
+9 −12
Original line number Diff line number Diff line
@@ -396,8 +396,7 @@ public class SizeCompatTests extends WindowTestsBase {
        opaqueActivity.removeImmediately();

        // Check that updateInheritedLetterbox() is invoked again
        verify(translucentActivity.mLetterboxUiController.getTransparentPolicy())
                .updateInheritedLetterbox();
        verify(translucentActivity.mLetterboxUiController.getTransparentPolicy()).start();
    }

    // TODO(b/333663877): Enable test after fix
@@ -466,8 +465,6 @@ public class SizeCompatTests extends WindowTestsBase {
        mTask.addChild(translucentActivity);
        // Transparent strategy applied
        assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
        assertNotNull(translucentActivity.mLetterboxUiController
                .getTransparentPolicy().mFirstOpaqueActivityBeneath);

        spyOn(translucentActivity.mLetterboxUiController.getTransparentPolicy());
        clearInvocations(translucentActivity.mLetterboxUiController.getTransparentPolicy());
@@ -475,11 +472,10 @@ public class SizeCompatTests extends WindowTestsBase {
        // We destroy the first opaque activity
        mActivity.removeImmediately();

        // Check that updateInheritedLetterbox() is invoked again on the TransparentPolicy
        verify(translucentActivity.mLetterboxUiController.getTransparentPolicy())
                .updateInheritedLetterbox();
        assertNull(translucentActivity.mLetterboxUiController
                .getTransparentPolicy().mFirstOpaqueActivityBeneath);
        // Check that start() is invoked again on the TransparentPolicy
        verify(translucentActivity.mLetterboxUiController.getTransparentPolicy()).start();
        assertFalse(translucentActivity.mLetterboxUiController
                .getTransparentPolicy().hasInheritedLetterboxBehavior());
    }

    @Test
@@ -507,11 +503,12 @@ public class SizeCompatTests extends WindowTestsBase {
        // Transparent strategy applied
        assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());

        spyOn(translucentActivity.mLetterboxUiController);
        clearInvocations(translucentActivity.mLetterboxUiController);
        spyOn(translucentActivity.mLetterboxUiController.getTransparentPolicy());
        clearInvocations(translucentActivity.mLetterboxUiController.getTransparentPolicy());

        // Check that updateInheritedLetterbox() is invoked again
        verify(translucentActivity.mLetterboxUiController, never()).updateInheritedLetterbox();
        verify(translucentActivity.mLetterboxUiController.getTransparentPolicy(), never())
                .start();
    }

    @Test