diff options
| -rw-r--r-- | services/core/java/com/android/server/wm/utils/OptPropFactory.java | 233 | ||||
| -rw-r--r-- | services/tests/wmtests/src/com/android/server/wm/utils/OptPropFactoryTest.java | 286 |
2 files changed, 519 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/wm/utils/OptPropFactory.java b/services/core/java/com/android/server/wm/utils/OptPropFactory.java new file mode 100644 index 000000000000..8201969b7c46 --- /dev/null +++ b/services/core/java/com/android/server/wm/utils/OptPropFactory.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2024 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.server.wm.utils; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.pm.PackageManager; +import android.util.Slog; + +import java.util.function.BooleanSupplier; + +/** + * Utility class which helps with handling with properties to opt-in or + * opt-out a specific feature. + */ +public class OptPropFactory { + + @NonNull + private final PackageManager mPackageManager; + + @NonNull + private final String mPackageName; + + /** + * Object responsible to handle optIn and optOut properties. + * + * @param packageManager The PackageManager reference + * @param packageName The name of the package. + */ + public OptPropFactory(@NonNull PackageManager packageManager, @NonNull String packageName) { + mPackageManager = packageManager; + mPackageName = packageName; + } + + /** + * Creates an OptProp for the given property + * + * @param propertyName The name of the property. + * @return The OptProp for the given property + */ + @NonNull + public OptProp create(@NonNull String propertyName) { + return OptProp.create( + () -> mPackageManager.getProperty(propertyName, mPackageName).getBoolean(), + propertyName); + } + + /** + * Creates an OptProp for the given property behind a gate condition. + * + * @param propertyName The name of the property. + * @param gateCondition If this resolves to false, the property is unset. This is evaluated at + * every interaction with the OptProp. + * @return The OptProp for the given property + */ + @NonNull + public OptProp create(@NonNull String propertyName, @NonNull BooleanSupplier gateCondition) { + return OptProp.create( + () -> mPackageManager.getProperty(propertyName, mPackageName).getBoolean(), + propertyName, + gateCondition); + } + + @FunctionalInterface + private interface ThrowableBooleanSupplier { + boolean get() throws Exception; + } + + public static class OptProp { + + private static final int VALUE_UNSET = -2; + private static final int VALUE_UNDEFINED = -1; + private static final int VALUE_FALSE = 0; + private static final int VALUE_TRUE = 1; + + @IntDef(prefix = {"VALUE_"}, value = { + VALUE_UNSET, + VALUE_UNDEFINED, + VALUE_FALSE, + VALUE_TRUE, + }) + @interface OptionalValue {} + + private static final String TAG = "OptProp"; + + // The condition is evaluated every time the OptProp state is accessed. + @NonNull + private final BooleanSupplier mCondition; + + // This is evaluated only once in the lifetime of an OptProp. + @NonNull + private final ThrowableBooleanSupplier mValueSupplier; + + @NonNull + private final String mPropertyName; + + @OptionalValue + private int mValue = VALUE_UNDEFINED; + + private OptProp(@NonNull ThrowableBooleanSupplier valueSupplier, + @NonNull String propertyName, + @NonNull BooleanSupplier condition) { + mValueSupplier = valueSupplier; + mPropertyName = propertyName; + mCondition = condition; + } + + @NonNull + private static OptProp create(@NonNull ThrowableBooleanSupplier valueSupplier, + @NonNull String propertyName) { + return new OptProp(valueSupplier, propertyName, () -> true); + } + + @NonNull + private static OptProp create(@NonNull ThrowableBooleanSupplier valueSupplier, + @NonNull String propertyName, @NonNull BooleanSupplier condition) { + return new OptProp(valueSupplier, propertyName, condition); + } + + /** + * @return {@code true} when the guarding condition is {@code true} and the property has + * been explicitly set to {@code true}. {@code false} otherwise. The guarding condition is + * evaluated every time this method is invoked. + */ + public boolean isTrue() { + return mCondition.getAsBoolean() && getValue() == VALUE_TRUE; + } + + /** + * @return {@code true} when the guarding condition is {@code true} and the property has + * been explicitly set to {@code false}. {@code false} otherwise. The guarding condition is + * evaluated every time this method is invoked. + */ + public boolean isFalse() { + return mCondition.getAsBoolean() && getValue() == VALUE_FALSE; + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} doesn't evaluate to {@code false} + * <li>App developers didn't opt out with a component {@code property} + * <li>App developers opted in with a component {@code property} or an OEM opted in with + * a per-app override + * </ul> + * + * <p>This is used for the treatments that are enabled only on per-app basis. + */ + public boolean shouldEnableWithOverrideAndProperty(boolean overrideValue) { + if (!mCondition.getAsBoolean()) { + return false; + } + if (getValue() == VALUE_FALSE) { + return false; + } + return getValue() == VALUE_TRUE || overrideValue; + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} doesn't evaluate to {@code false} + * <li>App developers didn't opt out with a component {@code property} + * <li>OEM opted in with a per-app override + * </ul> + * + * <p>This is used for the treatments that are enabled based with the heuristic but can be + * disabled on per-app basis by OEMs or app developers. + */ + public boolean shouldEnableWithOptInOverrideAndOptOutProperty( + boolean overrideValue) { + if (!mCondition.getAsBoolean()) { + return false; + } + return getValue() != VALUE_FALSE && overrideValue; + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} doesn't resolve to {@code false} + * <li>OEM didn't opt out with a per-app override + * <li>App developers didn't opt out with a component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled based with the heuristic but can be + * disabled on per-app basis by OEMs or app developers. + */ + public boolean shouldEnableWithOptOutOverrideAndProperty(boolean overrideValue) { + if (!mCondition.getAsBoolean()) { + return false; + } + return getValue() != VALUE_FALSE && !overrideValue; + } + + @OptionalValue + private int getValue() { + if (mValue == VALUE_UNDEFINED) { + try { + final Boolean value = mValueSupplier.get(); + if (TRUE.equals(value)) { + mValue = VALUE_TRUE; + } else if (FALSE.equals(value)) { + mValue = VALUE_FALSE; + } else { + mValue = VALUE_UNSET; + } + } catch (Exception e) { + Slog.w(TAG, "Cannot read opt property " + mPropertyName); + mValue = VALUE_UNSET; + } + } + return mValue; + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/OptPropFactoryTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/OptPropFactoryTest.java new file mode 100644 index 000000000000..004de1fc962b --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/utils/OptPropFactoryTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2024 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.server.wm.utils; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.pm.PackageManager; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.utils.OptPropFactory.OptProp; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.function.BooleanSupplier; + +/** + * Build/Install/Run: + * atest WmTests:OptPropFactoryTest + */ +@SmallTest +@Presubmit +public class OptPropFactoryTest { + + private PackageManager mPackageManager; + private OptPropFactory mOptPropFactory; + + @Before + public void setUp() { + mPackageManager = mock(PackageManager.class); + mOptPropFactory = new OptPropFactory(mPackageManager, ""); + } + + @Test + public void optProp_laziness() throws PackageManager.NameNotFoundException { + initPropAs(/* propertyValue */ true); + // When OptPropBuilder is created the PackageManager is not used + verify(mPackageManager, never()).getProperty(anyString(), anyString()); + + // Accessing the value multiple times only uses PackageManager once + final OptProp optProp = createOptProp(); + optProp.isTrue(); + optProp.isFalse(); + + verify(mPackageManager).getProperty(anyString(), anyString()); + } + + @Test + public void optProp_withSetValueTrue() throws PackageManager.NameNotFoundException { + initPropAs(/* propertyValue */ true); + + final OptProp optProp = createOptProp(); + + assertTrue(optProp.isTrue()); + assertFalse(optProp.isFalse()); + } + + @Test + public void optProp_withSetValueFalse() throws PackageManager.NameNotFoundException { + initPropAs(/* propertyValue */ false); + + final OptProp optProp = createOptProp(); + + assertFalse(optProp.isTrue()); + assertTrue(optProp.isFalse()); + } + + @Test + public void optProp_withSetValueWithConditionFalse() + throws PackageManager.NameNotFoundException { + initPropAs(/* propertyValue */ true); + + final OptProp optProp = createOptProp(() -> false); + + assertFalse(optProp.isTrue()); + assertFalse(optProp.isFalse()); + } + + @Test + public void optProp_withUnsetValue() { + final OptProp optProp = createOptProp(); + + assertFalse(optProp.isTrue()); + assertFalse(optProp.isFalse()); + } + + @Test + public void optProp_isUnsetWhenPropertyIsNotPresent() + throws PackageManager.NameNotFoundException { + initPropAsWithException(); + // Property is unset + final OptProp optUnset = createOptProp(); + assertFalse(optUnset.isTrue()); + assertFalse(optUnset.isFalse()); + } + + @Test + public void optProp_shouldEnableWithOverrideAndProperty() + throws PackageManager.NameNotFoundException { + // Property is unset + final OptProp optUnset = createOptProp(() -> false); + assertFalse(optUnset.shouldEnableWithOverrideAndProperty(/* override */ true)); + + // The value is the override one + final OptProp optUnsetOn = createOptProp(); + assertTrue(optUnsetOn.shouldEnableWithOverrideAndProperty(/* override */ true)); + assertFalse(optUnsetOn.shouldEnableWithOverrideAndProperty(/* override */ false)); + + // Property is set to true + initPropAs(true); + final OptProp optTrue = createOptProp(() -> false); + assertFalse(optTrue.shouldEnableWithOverrideAndProperty(/* override */ true)); + + final OptProp optTrueOn = createOptProp(() -> true); + assertTrue(optTrueOn.shouldEnableWithOverrideAndProperty(/* override */ true)); + assertTrue(optTrueOn.shouldEnableWithOverrideAndProperty(/* override */ false)); + + // Property is set to false + initPropAs(false); + final OptProp optFalse = createOptProp(() -> false); + assertFalse(optFalse.shouldEnableWithOverrideAndProperty(/* override */ true)); + + final OptProp optFalseOn = createOptProp(); + assertFalse(optFalseOn.shouldEnableWithOverrideAndProperty(/* override */ true)); + assertFalse(optFalseOn.shouldEnableWithOverrideAndProperty(/* override */ false)); + } + + @Test + public void optProp_shouldEnableWithOptInOverrideAndOptOutProperty() + throws PackageManager.NameNotFoundException { + // Property is unset + final OptProp optUnset = createOptProp(() -> false); + assertFalse(optUnset.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ true)); + + final OptProp optUnsetOn = createOptProp(); + assertTrue(optUnsetOn.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ true)); + assertFalse( + optUnsetOn.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ false)); + + // Property is set to true + initPropAs(true); + final OptProp optTrue = createOptProp(() -> false); + assertFalse(optTrue.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ true)); + + // Is the value of the override + final OptProp optTrueOn = createOptProp(() -> true); + assertTrue(optTrueOn.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ true)); + assertFalse(optTrueOn.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ false)); + + // Property is set to false + initPropAs(false); + final OptProp optFalse = createOptProp(() -> false); + assertFalse(optFalse.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ true)); + + // Always false ahatever is the value of the override + final OptProp optFalseOn = createOptProp(); + assertFalse(optFalseOn.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ true)); + assertFalse( + optFalseOn.shouldEnableWithOptInOverrideAndOptOutProperty(/* override */ false)); + } + + @Test + public void optProp_shouldEnableWithOptOutOverrideAndProperty() + throws PackageManager.NameNotFoundException { + // Property is unset + final OptProp optUnset = createOptProp(() -> false); + assertFalse(optUnset.shouldEnableWithOptOutOverrideAndProperty(/* override */ true)); + + // Is the negate of the override value + final OptProp optUnsetOn = createOptProp(); + assertTrue(optUnsetOn.shouldEnableWithOptOutOverrideAndProperty(/* override */ false)); + assertFalse(optUnsetOn.shouldEnableWithOptOutOverrideAndProperty(/* override */ true)); + + // Property is set to true + initPropAs(true); + final OptProp optTrue = createOptProp(() -> false); + assertFalse(optTrue.shouldEnableWithOptOutOverrideAndProperty(/* override */ true)); + + // Is the negate of the override value + final OptProp optTrueOn = createOptProp(() -> true); + assertTrue(optTrueOn.shouldEnableWithOptOutOverrideAndProperty(/* override */ false)); + assertFalse(optTrueOn.shouldEnableWithOptOutOverrideAndProperty(/* override */ true)); + + // Property is set to false + initPropAs(false); + final OptProp optFalse = createOptProp(() -> false); + assertFalse(optFalse.shouldEnableWithOptOutOverrideAndProperty(/* override */ true)); + + // Always false ahatever is the value of the override + final OptProp optFalseOn = createOptProp(); + assertFalse(optFalseOn.shouldEnableWithOptOutOverrideAndProperty(/* override */ true)); + assertFalse(optFalseOn.shouldEnableWithOptOutOverrideAndProperty(/* override */ false)); + } + + @Test + public void optProp_gateConditionIsInvokedOnlyOncePerInvocation() + throws PackageManager.NameNotFoundException { + + final FakeGateCondition trueCondition = new FakeGateCondition(/* returnValue */ true); + final OptProp optProp = createOptProp(trueCondition); + + optProp.shouldEnableWithOverrideAndProperty(/* override value */ true); + assertEquals(1, trueCondition.getInvocationCount()); + trueCondition.clearInvocationCount(); + + initPropAs(true); + optProp.shouldEnableWithOptInOverrideAndOptOutProperty(/* override value */ true); + assertEquals(1, trueCondition.getInvocationCount()); + trueCondition.clearInvocationCount(); + + optProp.shouldEnableWithOptOutOverrideAndProperty(/* override value */ true); + assertEquals(1, trueCondition.getInvocationCount()); + trueCondition.clearInvocationCount(); + } + + private void initPropAs(boolean propertyValue) throws PackageManager.NameNotFoundException { + Mockito.clearInvocations(mPackageManager); + final PackageManager.Property prop = new PackageManager.Property( + "", /* value */ propertyValue, "", ""); + when(mPackageManager.getProperty(anyString(), anyString())).thenReturn(prop); + } + + private void initPropAsWithException() throws PackageManager.NameNotFoundException { + Mockito.clearInvocations(mPackageManager); + when(mPackageManager.getProperty("", "")).thenThrow( + new PackageManager.NameNotFoundException()); + } + + private OptProp createOptProp() { + return mOptPropFactory.create(""); + } + + private OptProp createOptProp(BooleanSupplier condition) { + return mOptPropFactory.create("", condition); + } + + private static class FakeGateCondition implements BooleanSupplier { + + private int mInvocationCount = 0; + private final boolean mReturnValue; + + private FakeGateCondition(boolean returnValue) { + mReturnValue = returnValue; + } + + @Override + public boolean getAsBoolean() { + mInvocationCount++; + return mReturnValue; + } + + int getInvocationCount() { + return mInvocationCount; + } + + void clearInvocationCount() { + mInvocationCount = 0; + } + + } +} |