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

Commit 70f7b46d authored by Pierre Barbier de Reuille's avatar Pierre Barbier de Reuille
Browse files

Correct handling of desktop experience dev option

Makes sure not only the flag is enabled, but the device is eligible for
desktop mode.

As this is more expensive (e.g. requires getting the application
context, read system properties), move all the logic in
getToggleOverrideFromSystem, including checking for the feature to be
enabled or not, as this differs between the developer options.

Make DesktopExperienceFlags the same.

Flag: EXEMPT (bug fix)
Bug: 389092752
Fix: 414405404
Test: atest DesktopExperienceFlagsTest
Test: atest DesktopModeFlagsTest
Test: atest DesktopModeHelperTest
Test: Manual on various devices
Change-Id: I05b166c1fdd91c6688d7a0b717143a8ac7dfc392
parent 141940f3
Loading
Loading
Loading
Loading
+54 −6
Original line number Diff line number Diff line
@@ -22,10 +22,13 @@ import static com.android.server.display.feature.flags.Flags.enableDisplayConten

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.Context;
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.R;
import com.android.window.flags.Flags;

import java.util.ArrayList;
@@ -202,6 +205,25 @@ public enum DesktopExperienceFlags {
    // go/keep-sorted end
    ;

    /** Whether the desktop experience developer option is supported. */
    static boolean isDesktopExperienceDevOptionSupported() {
        if (!Flags.showDesktopExperienceDevOption()) {
            return false;
        }
        boolean shouldEnforceDeviceRestrictions = SystemProperties.getBoolean(
                "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
        if (!shouldEnforceDeviceRestrictions) {
            return true;
        }
        final Context context = getApplicationContext();
        if (context == null) {
            return false;
        }
        // Simplified version of DesktopModeHelper.isDeviceEligibleForDesktopMode, as the
        // developer option cannot be considered when we check eligibility.
        return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
    }

    /**
     * Flag class, to be used in case the enum cannot be used because the flag is not accessible.
     *
@@ -271,6 +293,10 @@ public enum DesktopExperienceFlags {
    @Nullable
    private static Boolean sCachedToggleOverride;

    // Local cache for the application context.
    @Nullable
    private static Context sApplicationContext;

    /**
     * Local cache of dynamically defined flag, organised by name.
     *
@@ -319,9 +345,7 @@ public enum DesktopExperienceFlags {

    private static boolean isFlagTrue(
            BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
        if (Flags.showDesktopExperienceDevOption()
                && shouldOverrideByDevOption
                && getToggleOverride()) {
        if (shouldOverrideByDevOption && getToggleOverride()) {
            return true;
        }
        return flagFunction.getAsBoolean();
@@ -357,14 +381,38 @@ public enum DesktopExperienceFlags {
        }

        // Otherwise, fetch and cache it
        boolean override = getToggleOverrideFromSystem();
        boolean override = isToggleOverriddenBySystem();
        sCachedToggleOverride = override;
        Log.d(TAG, "Toggle override initialized to: " + override);
        return override;
    }

    /** Returns the {@link ToggleOverride} from the system property.. */
    private static boolean getToggleOverrideFromSystem() {
    static Context getApplicationContext() {
        if (sApplicationContext == null) {
            final Context application = ActivityThread.currentApplication();
            if (application == null) {
                Log.w(TAG, "Could not get the current application.");
                return null;
            }
            sApplicationContext = application;
        }
        return sApplicationContext;
    }

    /** Returns whether the toggle is overridden by the relevant system property.. */
    private static boolean isToggleOverriddenBySystem() {
        // We never override if display content mode management is enabled.
        if (enableDisplayContentModeManagement()) {
            return false;
        }
        final Context context = getApplicationContext();
        if (context == null) {
            return false;
        }
        // If the developer option is not supported, we don't override.
        if (!isDesktopExperienceDevOptionSupported()) {
            return false;
        }
        return SystemProperties.getBoolean(SYSTEM_PROPERTY_NAME, false);
    }
}
+54 −44
Original line number Diff line number Diff line
@@ -16,13 +16,9 @@

package android.window;

import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement;

import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.Application;
import android.content.ContentResolver;
import android.os.SystemProperties;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;

@@ -191,8 +187,7 @@ public enum DesktopModeFlags {
    // Local cache for toggle override, which is initialized once on its first access. It needs to
    // be refreshed only on reboots as overridden state is expected to take effect on reboots.
    private static ToggleOverride sCachedToggleOverride;

    public static final String SYSTEM_PROPERTY_NAME = "persist.wm.debug.desktop_experience_devopts";
    private static ToggleOverride sCachedRawToggleOverride;

    DesktopModeFlags(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
        this.mFlagFunction = flagFunction;
@@ -208,77 +203,92 @@ public enum DesktopModeFlags {
    }

    public static boolean isDesktopModeForcedEnabled() {
        return getToggleOverride() == ToggleOverride.OVERRIDE_ON;
        return getRawToggleOverride() == ToggleOverride.OVERRIDE_ON;
    }

    private static boolean isFlagTrue(BooleanSupplier flagFunction,
            boolean shouldOverrideByDevOption) {
        if (!shouldOverrideByDevOption) return flagFunction.getAsBoolean();
        if (Flags.showDesktopExperienceDevOption()) {
            // If the feature is enabled, just return the flag's value.
            if (enableDisplayContentModeManagement()) {
                return flagFunction.getAsBoolean();
            }
            return switch (getToggleOverride()) {
                case OVERRIDE_UNSET, OVERRIDE_OFF -> flagFunction.getAsBoolean();
                case OVERRIDE_ON -> true;
            };
        }
        if (Flags.showDesktopWindowingDevOption()) {
            boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode();
        return switch (getToggleOverride()) {
            case OVERRIDE_UNSET -> flagFunction.getAsBoolean();
                // When toggle override matches its default state, don't override flags. This
                // helps users reset their feature overrides.
                case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && flagFunction.getAsBoolean();
                case OVERRIDE_ON -> !shouldToggleBeEnabledByDefault || flagFunction.getAsBoolean();
            case OVERRIDE_OFF -> false;
            case OVERRIDE_ON -> true;
        };
    }
        return flagFunction.getAsBoolean();
    }

    private static ToggleOverride getToggleOverride() {
        // If cached, return it
        if (sCachedToggleOverride != null) {
            return sCachedToggleOverride;
        }
        ToggleOverride override;
        // Otherwise, fetch and cache it
        ToggleOverride override = getToggleOverrideFromSystem();
        sCachedToggleOverride = override;
        if (sCachedRawToggleOverride == null) {
            override = getToggleOverrideFromSystem();
            sCachedRawToggleOverride = override;
        } else {
            override = sCachedRawToggleOverride;
        }
        // Override if the feature (i.e. enableDesktopWindowingMode) and the toggle are opposite.
        // That is, if desktop windowing mode is enabled but the toggle is disabled, we disable all
        // the flags while if desktop windowing mode is disabled but the toggle is enabled, we
        // enable all the flags.
        // If desktop windowing mode and the toggle agree or if the toggle is not set, we don't
        // touch the flags.
        ToggleOverride resolvedOverride;
        if (Flags.enableDesktopWindowingMode() && override == ToggleOverride.OVERRIDE_OFF) {
            resolvedOverride = ToggleOverride.OVERRIDE_OFF;
        } else if (!Flags.enableDesktopWindowingMode() && override == ToggleOverride.OVERRIDE_ON) {
            resolvedOverride = ToggleOverride.OVERRIDE_ON;
        } else {
            resolvedOverride = ToggleOverride.OVERRIDE_UNSET;
        }
        sCachedToggleOverride = resolvedOverride;
        Log.d(TAG, "Toggle override initialized to: " + override);
        return override;
        return resolvedOverride;
    }

    private static ToggleOverride getRawToggleOverride() {
        if (sCachedRawToggleOverride != null) {
            return sCachedRawToggleOverride;
        }
        getToggleOverride();
        return sCachedRawToggleOverride;
    }

    /**
     *  Returns {@link ToggleOverride} from Settings.Global set by toggle.
     */
    private static ToggleOverride getToggleOverrideFromSystem() {
        int settingValue;
        if (Flags.showDesktopExperienceDevOption()) {
            settingValue = SystemProperties.getInt(
                    SYSTEM_PROPERTY_NAME,
                    ToggleOverride.OVERRIDE_UNSET.getSetting()
            );
        } else {
            final Application application = ActivityThread.currentApplication();
            if (application == null) {
        if (DesktopExperienceFlags.isDesktopExperienceDevOptionSupported()) {
            if (DesktopExperienceFlags.getToggleOverride()) {
                return ToggleOverride.OVERRIDE_ON;
            }
            return ToggleOverride.OVERRIDE_UNSET;
        }

        if (Flags.showDesktopWindowingDevOption()) {
            final Context context = DesktopExperienceFlags.getApplicationContext();
            if (context == null) {
                Log.w(TAG, "Could not get the current application.");
                return ToggleOverride.OVERRIDE_UNSET;
            }
            final ContentResolver contentResolver = application.getContentResolver();
            final ContentResolver contentResolver = context.getContentResolver();
            if (contentResolver == null) {
                Log.w(TAG, "Could not get the content resolver for the application.");
                return ToggleOverride.OVERRIDE_UNSET;
            }
            settingValue = Settings.Global.getInt(
            int settingValue = Settings.Global.getInt(
                    contentResolver,
                    Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
                    ToggleOverride.OVERRIDE_UNSET.getSetting()
            );
        }
            return ToggleOverride.fromSetting(settingValue, ToggleOverride.OVERRIDE_UNSET);
        }

        return ToggleOverride.OVERRIDE_UNSET;
    }

    /** Override state of desktop mode developer option toggle. */
    public enum ToggleOverride {
        OVERRIDE_UNSET,
+29 −2
Original line number Diff line number Diff line
@@ -23,7 +23,12 @@ import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.FlagsParameterization;
@@ -34,6 +39,7 @@ import android.window.DesktopExperienceFlags.DesktopExperienceFlag;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.internal.R;
import com.android.window.flags.Flags;

import org.junit.After;
@@ -79,7 +85,10 @@ public class DesktopExperienceFlagsTest {

    private static final String OVERRIDE_OFF_SETTING = "0";
    private static final String OVERRIDE_ON_SETTING = "1";
    private Map<String, String> mInitialSyspropValues = null;
    private Map<String, String> mInitialSyspropValues = new HashMap<>();

    private Context mMockContext;
    private Resources mMockResources;

    public DesktopExperienceFlagsTest(FlagsParameterization flags) {
        mSetFlagsRule = new SetFlagsRule(flags);
@@ -87,7 +96,19 @@ public class DesktopExperienceFlagsTest {

    @Before
    public void setUp() throws Exception {
        mInitialSyspropValues = new HashMap<>();
        mInitialSyspropValues.clear();

        mMockResources = mock(Resources.class);
        mMockContext = mock(Context.class);
        when(mMockContext.getResources()).thenReturn(mMockResources);
        setDesktopModeSupported(false);

        // Set the application context to the mock one
        Field cachedToggleOverride =
                DesktopExperienceFlags.class.getDeclaredField("sApplicationContext");
        cachedToggleOverride.setAccessible(true);
        cachedToggleOverride.set(null, mMockContext);

        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        setSysProp(DesktopExperienceFlags.SYSTEM_PROPERTY_NAME, null);
    }
@@ -128,6 +149,7 @@ public class DesktopExperienceFlagsTest {
    @Test
    public void isTrue_devOptionEnabled_overrideOn_featureFlagOff() throws Exception {
        assumeTrue(Flags.showDesktopExperienceDevOption());
        setDesktopModeSupported(true);
        mLocalFlagValue = false;
        setSysProp(DesktopExperienceFlags.SYSTEM_PROPERTY_NAME, OVERRIDE_ON_SETTING);

@@ -170,4 +192,9 @@ public class DesktopExperienceFlagsTest {
        cachedToggleOverride.setAccessible(true);
        cachedToggleOverride.set(null, null);
    }

    private void setDesktopModeSupported(boolean isSupported) {
        when(mMockResources.getBoolean(eq(R.bool.config_isDesktopModeSupported))).thenReturn(
                isSupported);
    }
}
+107 −11

File changed.

Preview size limit exceeded, changes collapsed.

+40 −4
Original line number Diff line number Diff line
@@ -53,7 +53,7 @@ class DesktopModeStatusTest : ShellTestCase() {

    @Before
    fun setUp() {
        doReturn(mockResources).whenever(mockContext).getResources()
        doReturn(mockResources).whenever(mockContext).resources
        setDeviceEligibleForDesktopMode(false)
        doReturn(false).whenever(mockResources).getBoolean(
            eq(R.bool.config_isDesktopModeDevOptionSupported)
@@ -122,6 +122,20 @@ class DesktopModeStatusTest : ShellTestCase() {
        assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue()
    }

    @DisableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
        Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION
    )
    @Test
    fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_forceNotUsingDevOption_returnsFalse() {
        doReturn(true).whenever(mockResources).getBoolean(
            eq(R.bool.config_isDesktopModeDevOptionSupported)
        )
        setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_OFF)

        assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse()
    }

    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
    @Test
@@ -189,6 +203,20 @@ class DesktopModeStatusTest : ShellTestCase() {
        assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue()
    }

    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
        Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION
    )
    @Test
    fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_forceNotUsingDevOption_returnsFalse() {
        doReturn(true).whenever(mockResources).getBoolean(
            eq(R.bool.config_isDesktopModeDevOptionSupported)
        )
        setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_OFF)

        assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse()
    }

    @Test
    fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() {
        doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
@@ -284,11 +312,17 @@ class DesktopModeStatusTest : ShellTestCase() {
    }

    private fun resetDesktopModeFlagsCache() {
        // Toggle raw override cache for DesktopModeFlags
        val cachedRawToggleOverride1 =
            DesktopModeFlags::class.java.getDeclaredField("sCachedRawToggleOverride")
        cachedRawToggleOverride1.isAccessible = true
        cachedRawToggleOverride1.set(null, DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET)

        // Toggle override cache for DesktopModeFlags
        val cachedToggleOverride1 =
            DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride")
        cachedToggleOverride1.isAccessible = true
        cachedToggleOverride1.set(null, DesktopModeFlags.ToggleOverride.OVERRIDE_OFF)
        cachedToggleOverride1.set(null, null)

        // Toggle override cache for DesktopExperienceFlags
        val cachedToggleOverride2 =
@@ -298,9 +332,11 @@ class DesktopModeStatusTest : ShellTestCase() {
    }

    private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) {
        // Toggle override cache for DesktopModeFlags can be on/off/unset
        resetDesktopModeFlagsCache()

        // Toggle raw override cache for DesktopModeFlags can be on/off/unset
        val cachedToggleOverride1 =
            DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride")
            DesktopModeFlags::class.java.getDeclaredField("sCachedRawToggleOverride")
        cachedToggleOverride1.isAccessible = true
        cachedToggleOverride1.set(null, override)

Loading