diff options
38 files changed, 1603 insertions, 332 deletions
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java index 6629fdc4cdee..16727c30dfd4 100644 --- a/core/java/android/webkit/WebViewProviderInfo.java +++ b/core/java/android/webkit/WebViewProviderInfo.java @@ -23,6 +23,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Base64; +import java.util.Arrays; +import java.util.Objects; + /** * @hide */ @@ -80,6 +83,35 @@ public final class WebViewProviderInfo implements Parcelable { out.writeTypedArray(signatures, 0); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof WebViewProviderInfo that) { + return this.packageName.equals(that.packageName) + && this.description.equals(that.description) + && this.availableByDefault == that.availableByDefault + && this.isFallback == that.isFallback + && Arrays.equals(this.signatures, that.signatures); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(packageName, description, availableByDefault, + isFallback, Arrays.hashCode(signatures)); + } + + @Override + public String toString() { + return "WebViewProviderInfo; packageName=" + packageName + + " description=\"" + description + + "\" availableByDefault=" + availableByDefault + + " isFallback=" + isFallback + + " signatures=" + Arrays.toString(signatures); + } + // fields read from framework resource public final String packageName; public final String description; diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp new file mode 100644 index 000000000000..1fb5f2c0789b --- /dev/null +++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp @@ -0,0 +1,30 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "BatteryUsageStatsProtoTests", + srcs: ["src/**/*.java"], + + static_libs: [ + "androidx.test.rules", + "junit", + "mockito-target-minus-junit4", + "platform-test-annotations", + "platformprotosnano", + "statsdprotolite", + "truth", + ], + + libs: ["android.test.runner"], + + platform_apis: true, + certificate: "platform", + + test_suites: ["device-tests"], +} diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml new file mode 100644 index 000000000000..9128dca2080b --- /dev/null +++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.core.batteryusagestatsprototests"> + + <uses-permission android:name="android.permission.BATTERY_STATS"/> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.core.batteryusagestatsprototests" + android:label="BatteryUsageStats Proto Tests" /> + +</manifest> diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java index 62efbc3cfa35..ac1f7d0e345f 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java +++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.server.power.stats; +package com.android.internal.os; import static android.os.BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE; @@ -23,262 +23,39 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import android.os.AggregateBatteryConsumer; import android.os.BatteryConsumer; import android.os.BatteryUsageStats; -import android.os.Process; import android.os.UidBatteryConsumer; import android.os.nano.BatteryUsageStatsAtomsProto; import android.os.nano.BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage; -import android.platform.test.ravenwood.RavenwoodRule; -import android.util.StatsEvent; import androidx.test.filters.SmallTest; -import com.android.server.am.BatteryStatsService; - import com.google.protobuf.nano.InvalidProtocolBufferNanoException; -import org.junit.Rule; import org.junit.Test; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -@SmallTest -public class BatteryUsageStatsAtomTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); +@SmallTest +public class BatteryUsageStatsPulledTest { private static final int UID_0 = 1000; private static final int UID_1 = 2000; private static final int UID_2 = 3000; private static final int UID_3 = 4000; + private static final int[] UID_USAGE_TIME_PROCESS_STATES = { + BatteryConsumer.PROCESS_STATE_FOREGROUND, + BatteryConsumer.PROCESS_STATE_BACKGROUND, + BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE + }; @Test - public void testAtom_BatteryUsageStatsPerUid() { - final BatteryUsageStats bus = buildBatteryUsageStats(); - BatteryStatsService.FrameworkStatsLogger statsLogger = - mock(BatteryStatsService.FrameworkStatsLogger.class); - - List<StatsEvent> actual = new ArrayList<>(); - new BatteryStatsService.StatsPerUidLogger(statsLogger).logStats(bus, actual); - - // Device-wide totals - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - Process.INVALID_UID, - BatteryConsumer.PROCESS_STATE_UNSPECIFIED, - 0L, - "cpu", - 30000.0f, - 20100.0f, - 20300L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - Process.INVALID_UID, - BatteryConsumer.PROCESS_STATE_UNSPECIFIED, - 0L, - "camera", - 30000.0f, - 20150.0f, - 0L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - Process.INVALID_UID, - BatteryConsumer.PROCESS_STATE_UNSPECIFIED, - 0L, - "CustomConsumer1", - 30000.0f, - 20200.0f, - 20400L - ); - - // Per-proc state estimates for UID_0 - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_UNSPECIFIED, - 0L, - "screen", - 1650.0f, - 300.0f, - 0L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_UNSPECIFIED, - 0L, - "cpu", - 1650.0f, - 400.0f, - 600L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_FOREGROUND, - 1000L, - "cpu", - 1650.0f, - 9100.0f, - 8100L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_BACKGROUND, - 2000L, - "cpu", - 1650.0f, - 9200.0f, - 8200L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE, - 0L, - "cpu", - 1650.0f, - 9300.0f, - 8400L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_CACHED, - 0L, - "cpu", - 1650.0f, - 9400.0f, - 0L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_FOREGROUND, - 1000L, - "CustomConsumer1", - 1650.0f, - 450.0f, - 0L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_BACKGROUND, - 2000L, - "CustomConsumer1", - 1650.0f, - 450.0f, - 0L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_FOREGROUND, - 1000L, - "CustomConsumer2", - 1650.0f, - 500.0f, - 800L - ); - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_0, - BatteryConsumer.PROCESS_STATE_BACKGROUND, - 2000L, - "CustomConsumer2", - 1650.0f, - 500.0f, - 800L - ); - - // Nothing for UID_1, because its power consumption is 0 - - // Only "screen" is populated for UID_2 - verify(statsLogger).buildStatsEvent( - 1000L, - 20000L, - 10000L, - 20, - 1234L, - UID_2, - BatteryConsumer.PROCESS_STATE_UNSPECIFIED, - 0L, - "screen", - 766.0f, - 766.0f, - 0L - ); - - verifyNoMoreInteractions(statsLogger); - } - - @Test - public void testAtom_BatteryUsageStatsAtomsProto() { + public void testGetStatsProto() { final BatteryUsageStats bus = buildBatteryUsageStats(); final byte[] bytes = bus.getStatsProto(); BatteryUsageStatsAtomsProto proto; @@ -291,7 +68,9 @@ public class BatteryUsageStatsAtomTest { assertEquals(bus.getStatsStartTimestamp(), proto.sessionStartMillis); assertEquals(bus.getStatsEndTimestamp(), proto.sessionEndMillis); - assertEquals(10000, proto.sessionDurationMillis); + assertEquals( + bus.getStatsEndTimestamp() - bus.getStatsStartTimestamp(), + proto.sessionDurationMillis); assertEquals(bus.getDischargePercentage(), proto.sessionDischargePercentage); assertEquals(bus.getDischargeDurationMs(), proto.dischargeDurationMillis); @@ -311,8 +90,8 @@ public class BatteryUsageStatsAtomTest { final List<android.os.UidBatteryConsumer> uidConsumers = bus.getUidBatteryConsumers(); uidConsumers.sort((a, b) -> a.getUid() - b.getUid()); - final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto = - proto.uidBatteryConsumers; + final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto + = proto.uidBatteryConsumers; Arrays.sort(uidConsumersProto, (a, b) -> a.uid - b.uid); // UID_0 - After sorting, UID_0 should be in position 0 for both data structures @@ -407,12 +186,6 @@ public class BatteryUsageStatsAtomTest { } } - private static final int[] UID_USAGE_TIME_PROCESS_STATES = { - BatteryConsumer.PROCESS_STATE_FOREGROUND, - BatteryConsumer.PROCESS_STATE_BACKGROUND, - BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE - }; - private void assertSameUidBatteryConsumer( android.os.UidBatteryConsumer uidConsumer, BatteryUsageStatsAtomsProto.UidBatteryConsumer uidConsumerProto, @@ -422,10 +195,10 @@ public class BatteryUsageStatsAtomTest { assertEquals("Uid consumers had mismatched uids", uid, uidConsumer.getUid()); assertEquals("For uid " + uid, - uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_FOREGROUND), + uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND), uidConsumerProto.timeInForegroundMillis); assertEquals("For uid " + uid, - uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_BACKGROUND), + uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND), uidConsumerProto.timeInBackgroundMillis); for (int processState : UID_USAGE_TIME_PROCESS_STATES) { final long timeInStateMillis = uidConsumer.getTimeInProcessStateMs(processState); @@ -492,9 +265,7 @@ public class BatteryUsageStatsAtomTest { .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) .setDischargeDurationMs(1234) - .setStatsStartTimestamp(1000) - .setStatsEndTimestamp(20000) - .setStatsDuration(10000); + .setStatsStartTimestamp(1000); final UidBatteryConsumer.Builder uidBuilder = builder .getOrCreateUidBatteryConsumerBuilder(UID_0) .setPackageWithHighestDrain("myPackage0") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 9bcd9b0a11c8..99bd96e722fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -3739,8 +3739,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", - "app package " + taskInfo.baseActivity.getPackageName() - + " does not support splitscreen, or is a controlled activity type")); + "app package " + taskInfo.baseIntent.getComponent() + + " does not support splitscreen, or is a controlled activity" + + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt index db962e717a3b..2406bdeebdf2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt @@ -48,7 +48,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Before fun setup() { - tapl.workspace.switchToOverview().dismissAllTasks() + val overview = tapl.workspace.switchToOverview() + if (overview.hasTasks()) { + overview.dismissAllTasks() + } tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 7886e85cbad8..49b974fa3f00 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -20,6 +20,7 @@ import android.app.NotificationManager import android.provider.Settings import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.notification.modes.ZenMode +import java.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -35,8 +36,7 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = - MutableStateFlow(listOf(TestModeBuilder.EXAMPLE)) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -52,6 +52,10 @@ class FakeZenModeRepository : ZenModeRepository { mutableZenMode.value = zenMode } + fun addModes(zenModes: List<ZenMode>) { + mutableModesFlow.value += zenModes + } + fun addMode(id: String, active: Boolean = false) { mutableModesFlow.value += newMode(id, active) } @@ -60,6 +64,20 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id } } + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + activateMode(zenMode.id) + } + + override fun deactivateMode(zenMode: ZenMode) { + deactivateMode(zenMode.id) + } + + fun activateMode(id: String) { + val oldMode = mutableModesFlow.value.find { it.id == id } ?: return + removeMode(id) + mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build() + } + fun deactivateMode(id: String) { val oldMode = mutableModesFlow.value.find { it.id == id } ?: return removeMode(id) diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index b2fcb5f6da41..0ff7f84a08b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -30,6 +30,7 @@ import android.provider.Settings import com.android.settingslib.flags.Flags import com.android.settingslib.notification.modes.ZenMode import com.android.settingslib.notification.modes.ZenModesBackend +import java.time.Duration import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose @@ -57,6 +58,10 @@ interface ZenModeRepository { /** A list of all existing priority modes. */ val modes: Flow<List<ZenMode>> + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) + + fun deactivateMode(zenMode: ZenMode) } @SuppressLint("SharedFlowCreation") @@ -178,4 +183,12 @@ class ZenModeRepositoryImpl( flowOf(emptyList()) } } + + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + backend.activateMode(zenMode, duration) + } + + override fun deactivateMode(zenMode: ZenMode) { + backend.deactivateMode(zenMode) + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 7b994d59d963..2f7cdd617081 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -37,6 +37,13 @@ public class TestModeBuilder { private ZenModeConfig.ZenRule mConfigZenRule; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); + public static final ZenMode MANUAL_DND = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd")) + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(), + true /* isActive */ + ); public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index a17076b525f4..4e01a71df113 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -539,6 +539,7 @@ android_library { "androidx.preference_preference", "androidx.appcompat_appcompat", "androidx.concurrent_concurrent-futures", + "androidx.concurrent_concurrent-futures-ktx", "androidx.mediarouter_mediarouter", "androidx.palette_palette", "androidx.legacy_legacy-preference-v14", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 97a45fb7b16a..035e2fbf8e65 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -870,6 +870,13 @@ flag { } flag { + name: "qs_ui_refactor_compose_fragment" + namespace: "systemui" + description: "Uses a different QS fragment in NPVC that uses the new compose UI and recommended architecture. This flag depends on qs_ui_refactor flag." + bug: "325099249" +} + +flag { name: "remove_dream_overlay_hide_on_touch" namespace: "systemui" description: "Removes logic to hide the dream overlay on user interaction, as it conflicts with various transitions" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt new file mode 100644 index 000000000000..fdfc7f13abf7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -0,0 +1,164 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesDialogViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + val repository = kosmos.fakeZenModeRepository + val interactor = kosmos.zenModeInteractor + + val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) + + @Test + fun tiles_filtersOutDisabledModes() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder().setName("Disabled").setEnabled(false).build(), + TestModeBuilder.MANUAL_DND, + TestModeBuilder() + .setName("Enabled") + .setEnabled(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Disabled with manual") + .setEnabled(false) + .setManualInvocationAllowed(true) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(2) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Manual DND") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Enabled") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun tiles_filtersOutInactiveModesWithoutManualInvocation() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + TestModeBuilder() + .setName("Active with manual") + .setTriggerDescription("trigger description") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive with manual") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive without manual") + .setActive(false) + .setManualInvocationAllowed(false) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(3) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Active with manual") + assertThat(this.subtext).isEqualTo("trigger description") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(2)!!) { + assertThat(this.text).isEqualTo("Inactive with manual") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun onClick_togglesTileState() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + val modeId = "id" + repository.addModes( + listOf( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles?.elementAt(0)?.enabled).isFalse() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isTrue() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isFalse() + } +} diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index be2850448ca5..8322b6c85aed 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1093,6 +1093,12 @@ <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] --> <string name="zen_modes_dialog_settings">Settings</string> + <!-- Priority modes: label for an active mode [CHAR LIMIT=35] --> + <string name="zen_mode_on">On</string> + + <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] --> + <string name="zen_mode_off">Off</string> + <!-- Zen mode: Priority only introduction message on first use --> <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string> diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index af7ecf66d107..1ba274ff4e76 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -28,6 +28,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen +import com.android.systemui.qs.flags.NewQsUI +import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag @@ -66,14 +68,20 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() + + // QS Fragment using Compose dependencies + QSComposeFragment.token dependsOn NewQsUI.token } private inline val politeNotifications get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications()) + private inline val crossAppPoliteNotifications get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) + private inline val vibrateWhileUnlockedToken: FlagToken get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) + private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt index 8af566523b67..ee709c4cf41a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt @@ -20,7 +20,7 @@ import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils -/** Helper for reading or using the notification avalanche suppression flag state. */ +/** Helper for reading or using the new QS UI flag state. */ @Suppress("NOTHING_TO_INLINE") object NewQsUI { /** The aconfig flag name */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt new file mode 100644 index 000000000000..664d49607f89 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt @@ -0,0 +1,53 @@ +/* + * 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.systemui.qs.flags + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the new QS UI in NPVC flag state. */ +@Suppress("NOTHING_TO_INLINE") +object QSComposeFragment { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 4f6a64f043d2..bd0868530cba 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1265,20 +1265,20 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mTranslationForFullShadeTransition = qsTranslation; updateQsFrameTranslation(); float currentTranslation = mQsFrame.getTranslationY(); - int clipTop = mEnableClipping - ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0; - int clipBottom = mEnableClipping - ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0; + int clipTop = (int) (top - currentTranslation - mQsFrame.getTop()); + int clipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop()); mVisible = qsVisible; mQs.setQsVisible(qsVisible); - mQs.setFancyClipping( - mDisplayLeftInset, - clipTop, - mDisplayRightInset, - clipBottom, - radius, - qsVisible && !mSplitShadeEnabled, - mIsFullWidth); + if (mEnableClipping) { + mQs.setFancyClipping( + mDisplayLeftInset, + clipTop, + mDisplayRightInset, + clipBottom, + radius, + qsVisible && !mSplitShadeEnabled, + mIsFullWidth); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index e4d06681d439..7a521a6ba28f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -16,8 +16,14 @@ package com.android.systemui.statusbar.policy.domain.interactor +import android.content.Context import android.provider.Settings +import androidx.concurrent.futures.await import com.android.settingslib.notification.data.repository.ZenModeRepository +import com.android.settingslib.notification.modes.ZenIconLoader +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.common.shared.model.Icon +import java.time.Duration import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -28,7 +34,9 @@ import kotlinx.coroutines.flow.map * An interactor that performs business logic related to the status and configuration of Zen Mode * (or Do Not Disturb/DND Mode). */ -class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { +class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) { + private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance() + val isZenModeEnabled: Flow<Boolean> = repository.globalZenMode .map { @@ -52,4 +60,18 @@ class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { } } .distinctUntilChanged() + + val modes: Flow<List<ZenMode>> = repository.modes + + suspend fun getModeIcon(mode: ZenMode, context: Context): Icon { + return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null) + } + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) { + repository.activateMode(zenMode, duration) + } + + fun deactivateMode(zenMode: ZenMode) { + repository.deactivateMode(zenMode) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index 6db1eacaa706..2b094d6b4922 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -29,6 +29,8 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create +import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import javax.inject.Inject class ModesDialogDelegate @@ -37,12 +39,13 @@ constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, + private val viewModel: ModesDialogViewModel, ) : SystemUIDialog.Delegate { override fun createDialog(): SystemUIDialog { return sysuiDialogFactory.create { dialog -> AlertDialogContent( title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, - content = { Text("Under construction") }, + content = { ModeTileGrid(viewModel) }, neutralButton = { PlatformOutlinedButton( onClick = { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt new file mode 100644 index 000000000000..91bfdff1095e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -0,0 +1,91 @@ +/* + * 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.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel + +@Composable +fun ModeTile(viewModel: ModeTileViewModel) { + val tileColor = + if (viewModel.enabled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant + val contentColor = + if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Surface( + color = tileColor, + shape = RoundedCornerShape(16.dp), + modifier = + Modifier.combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick + ), + ) { + Row( + modifier = Modifier.padding(20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + Arrangement.spacedBy( + space = 10.dp, + alignment = Alignment.Start, + ), + ) { + Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp)) + Column { + Text( + viewModel.text, + fontWeight = FontWeight.W500, + modifier = Modifier.tileMarquee() + ) + Text( + viewModel.subtext, + fontWeight = FontWeight.W400, + modifier = Modifier.tileMarquee() + ) + } + } + } + } +} + +private fun Modifier.tileMarquee(): Modifier { + return this.basicMarquee( + iterations = 1, + initialDelayMillis = 200, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt new file mode 100644 index 000000000000..73d361f69eac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -0,0 +1,50 @@ +/* + * 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.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel + +@Composable +fun ModeTileGrid(viewModel: ModesDialogViewModel) { + val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) + + // TODO(b/346519570): Handle what happens when we have more than a few modes. + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + tiles.size, + key = { index -> tiles[index].id }, + ) { index -> + ModeTile(viewModel = tiles[index]) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt new file mode 100644 index 000000000000..5bd26ccc965f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt @@ -0,0 +1,35 @@ +/* + * 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.systemui.statusbar.policy.ui.dialog.viewmodel + +import com.android.systemui.common.shared.model.Icon + +/** + * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes + * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the + * dialog. + */ +data class ModeTileViewModel( + val id: String, + val icon: Icon, + val text: String, + val subtext: String, + val enabled: Boolean, + val contentDescription: String, + val onClick: () -> Unit, + val onLongClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt new file mode 100644 index 000000000000..e84c8b61ff54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -0,0 +1,87 @@ +/* + * 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.systemui.statusbar.policy.ui.dialog.viewmodel + +import android.content.Context +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows + * the user to quickly toggle modes. + */ +@SysUISingleton +class ModesDialogViewModel +@Inject +constructor( + val context: Context, + zenModeInteractor: ZenModeInteractor, + @Background val bgDispatcher: CoroutineDispatcher, +) { + // Modes that should be displayed in the dialog + // TODO(b/346519570): Include modes that have not been set up yet. + private val visibleModes: Flow<List<ZenMode>> = + zenModeInteractor.modes.map { + it.filter { mode -> + mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed) + } + } + + val tiles: Flow<List<ModeTileViewModel>> = + visibleModes + .map { modesList -> + modesList.map { mode -> + ModeTileViewModel( + id = mode.id, + icon = zenModeInteractor.getModeIcon(mode, context), + text = mode.rule.name, + subtext = getTileSubtext(mode), + enabled = mode.isActive, + // TODO(b/346519570): This should be some combination of the above, e.g. + // "ON: Do Not Disturb, Until Mon 08:09"; see DndTile. + contentDescription = "", + onClick = { + if (mode.isActive) { + zenModeInteractor.deactivateMode(mode) + } else { + // TODO(b/346519570): Handle duration for DND mode. + zenModeInteractor.activateMode(mode) + } + }, + onLongClick = { + // TODO(b/346519570): Open settings page for mode. + } + ) + } + } + .flowOn(bgDispatcher) + + private fun getTileSubtext(mode: ZenMode): String { + // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND + val on = context.resources.getString(R.string.zen_mode_on) + val off = context.resources.getString(R.string.zen_mode_off) + return mode.rule.triggerDescription ?: if (mode.isActive) on else off + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 49e3f04cb44e..31f93b402a75 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1059,6 +1059,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer + @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART) public void testShowBouncerOrKeyguard_needsFullScreen() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java new file mode 100644 index 000000000000..db95fad2a3ad --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java @@ -0,0 +1,51 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail01_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail01_Test() { + mThrown.expectMessage("Method setUp() doesn't have @Before"); + } + + @SuppressWarnings("JUnit4SetUpNotRun") + public void setUp() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java new file mode 100644 index 000000000000..ddc66c73a7c0 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java @@ -0,0 +1,51 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail02_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail02_Test() { + mThrown.expectMessage("Method tearDown() doesn't have @After"); + } + + @SuppressWarnings("JUnit4TearDownNotRun") + public void tearDown() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java new file mode 100644 index 000000000000..ec8e907dcdb3 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java @@ -0,0 +1,51 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail03_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail03_Test() { + mThrown.expectMessage("Method testFoo() doesn't have @Test"); + } + + @SuppressWarnings("JUnit4TestNotRun") + public void testFoo() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java new file mode 100644 index 000000000000..d952d07b3817 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java @@ -0,0 +1,56 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_OkTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Before + public void setUp() { + } + + @Before + public void testSetUp() { + } + + @After + public void tearDown() { + } + + @After + public void testTearDown() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 49e793fcbddf..4357f2b8660a 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -33,14 +33,17 @@ import com.android.internal.os.RuntimeInit; import com.android.server.LocalServices; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runners.model.Statement; import java.io.PrintStream; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -230,6 +233,18 @@ public class RavenwoodRuleImpl { } } + /** + * @return if a method has any of annotations. + */ + private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) { + for (var anno : annotations) { + if (m.getAnnotation(anno) != null) { + return true; + } + } + return false; + } + private static void validateTestAnnotations(Statement base, Description description, boolean enableOptionalValidation) { final var testClass = description.getTestClass(); @@ -239,13 +254,14 @@ public class RavenwoodRuleImpl { boolean hasErrors = false; for (Method m : collectMethods(testClass)) { if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) { - if (m.getAnnotation(Test.class) == null) { + if (!hasAnyAnnotations(m, Test.class, Before.class, After.class, + BeforeClass.class, AfterClass.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @Test"); hasErrors = true; } } if ("setUp".equals(m.getName())) { - if (m.getAnnotation(Before.class) == null) { + if (!hasAnyAnnotations(m, Before.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @Before"); hasErrors = true; } @@ -255,7 +271,7 @@ public class RavenwoodRuleImpl { } } if ("tearDown".equals(m.getName())) { - if (m.getAnnotation(After.class) == null) { + if (!hasAnyAnnotations(m, After.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @After"); hasErrors = true; } diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index d7da2f0052d3..a5ec2ba2f267 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -804,7 +804,15 @@ public final class PresentationStatsEventLogger { + event.mSuggestionPresentedLastTimestampMs + " event.mFocusedVirtualAutofillId=" + event.mFocusedVirtualAutofillId + " event.mFieldFirstLength=" + event.mFieldFirstLength - + " event.mFieldLastLength=" + event.mFieldLastLength); + + " event.mFieldLastLength=" + event.mFieldLastLength + + " event.mViewFailedPriorToRefillCount=" + event.mViewFailedPriorToRefillCount + + " event.mViewFilledSuccessfullyOnRefillCount=" + + event.mViewFilledSuccessfullyOnRefillCount + + " event.mViewFailedOnRefillCount=" + event.mViewFailedOnRefillCount + + " event.notExpiringResponseDuringAuthCount=" + + event.mFixExpireResponseDuringAuthCount + + " event.notifyViewEnteredIgnoredDuringAuthCount=" + + event.mNotifyViewEnteredIgnoredDuringAuthCount); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -859,7 +867,12 @@ public final class PresentationStatsEventLogger { event.mSuggestionPresentedLastTimestampMs, event.mFocusedVirtualAutofillId, event.mFieldFirstLength, - event.mFieldLastLength); + event.mFieldLastLength, + event.mViewFailedPriorToRefillCount, + event.mViewFilledSuccessfullyOnRefillCount, + event.mViewFailedOnRefillCount, + event.mFixExpireResponseDuringAuthCount, + event.mNotifyViewEnteredIgnoredDuringAuthCount); mEventInternal = Optional.empty(); } @@ -912,6 +925,12 @@ public final class PresentationStatsEventLogger { // uninitialized doesn't help much, as this would be non-zero only if callback is received. int mViewFillSuccessCount = 0; int mViewFilledButUnexpectedCount = 0; + int mViewFailedPriorToRefillCount = 0; + int mViewFailedOnRefillCount = 0; + int mViewFilledSuccessfullyOnRefillCount = 0; + + int mFixExpireResponseDuringAuthCount = 0; + int mNotifyViewEnteredIgnoredDuringAuthCount = 0; ArraySet<AutofillId> mAutofillIdsAttemptedAutofill; ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>(); diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 092ee16f3342..67985efcd7bc 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -102,7 +102,6 @@ import android.util.StatsEvent; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; import com.android.internal.os.BinderCallsStats; import com.android.internal.os.Clock; @@ -1192,7 +1191,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub .setMinConsumedPowerThreshold(minConsumedPowerThreshold) .build(); bus = getBatteryUsageStats(List.of(query)).get(0); - return new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data); + return StatsPerUidLogger.logStats(bus, data); } default: throw new UnsupportedOperationException("Unknown tagId=" + atomTag); @@ -1205,35 +1204,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } - public static class FrameworkStatsLogger { - /** - * Wrapper for the FrameworkStatsLog.buildStatsEvent method that makes it easier - * for mocking. - */ - @VisibleForTesting - public StatsEvent buildStatsEvent(long sessionStartTs, long sessionEndTs, - long sessionDuration, int sessionDischargePercentage, long sessionDischargeDuration, - int uid, @BatteryConsumer.ProcessState int processState, long timeInStateMillis, - String powerComponentName, float totalConsumedPowerMah, float powerComponentMah, - long powerComponentDurationMillis) { - return FrameworkStatsLog.buildStatsEvent( - FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID, - sessionStartTs, - sessionEndTs, - sessionDuration, - sessionDischargePercentage, - sessionDischargeDuration, - uid, - processState, - timeInStateMillis, - powerComponentName, - totalConsumedPowerMah, - powerComponentMah, - powerComponentDurationMillis); - } - } - - public static class StatsPerUidLogger { + private static class StatsPerUidLogger { private static final int STATSD_METRIC_MAX_DIMENSIONS_COUNT = 3000; @@ -1253,18 +1224,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub long dischargeDuration) {} ; - private final FrameworkStatsLogger mFrameworkStatsLogger; - - public StatsPerUidLogger(FrameworkStatsLogger frameworkStatsLogger) { - mFrameworkStatsLogger = frameworkStatsLogger; - } - - /** - * Generates StatsEvents for the supplied battery usage stats and adds them to - * the supplied list. - */ - @VisibleForTesting - public int logStats(BatteryUsageStats bus, List<StatsEvent> data) { + static int logStats(BatteryUsageStats bus, List<StatsEvent> data) { final SessionInfo sessionInfo = new SessionInfo( bus.getStatsStartTimestamp(), @@ -1380,7 +1340,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub return StatsManager.PULL_SUCCESS; } - private boolean addStatsForPredefinedComponent( + private static boolean addStatsForPredefinedComponent( List<StatsEvent> data, SessionInfo sessionInfo, int uid, @@ -1420,7 +1380,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub powerComponentDurationMillis); } - private boolean addStatsForCustomComponent( + private static boolean addStatsForCustomComponent( List<StatsEvent> data, SessionInfo sessionInfo, int uid, @@ -1462,7 +1422,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub * Returns true on success and false if reached max atoms capacity and no more atoms should * be added */ - private boolean addStatsAtom( + private static boolean addStatsAtom( List<StatsEvent> data, SessionInfo sessionInfo, int uid, @@ -1472,7 +1432,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub float totalConsumedPowerMah, float powerComponentMah, long powerComponentDurationMillis) { - data.add(mFrameworkStatsLogger.buildStatsEvent( + data.add( + FrameworkStatsLog.buildStatsEvent( + FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID, sessionInfo.startTs(), sessionInfo.endTs(), sessionInfo.duration(), diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index 1f341147deb1..e0c0c2c60123 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -22,7 +22,7 @@ import android.os.SystemProperties; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; +import com.android.server.wm.utils.DesktopModeFlagsUtil; /** * Constants for desktop mode feature @@ -35,8 +35,8 @@ public final class DesktopModeHelper { "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); /** Whether desktop mode is enabled. */ - static boolean isDesktopModeEnabled() { - return Flags.enableDesktopWindowingMode(); + static boolean isDesktopModeEnabled(@NonNull Context context) { + return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context); } /** @@ -60,7 +60,7 @@ public final class DesktopModeHelper { * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return isDesktopModeEnabled() + return isDesktopModeEnabled(context) && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context)); } } diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java new file mode 100644 index 000000000000..4211764085b1 --- /dev/null +++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java @@ -0,0 +1,173 @@ +/* + * 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 com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET; + +import android.annotation.Nullable; +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import com.android.window.flags.Flags; + +import java.util.function.Supplier; + +/** + * Util to check desktop mode flags state. + * + * This utility is used to allow developer option toggles to override flags related to desktop + * windowing. + * + * Computes whether Desktop Windowing related flags should be enabled by using the aconfig flag + * value and the developer option override state (if applicable). + * + * This is a partial copy of {@link com.android.wm.shell.shared.desktopmode.DesktopModeFlags} which + * is to be used in WM core. + */ +public enum DesktopModeFlagsUtil { + // All desktop mode related flags to be overridden by developer option toggle will be added here + DESKTOP_WINDOWING_MODE( + Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true), + WALLPAPER_ACTIVITY( + Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true); + + private static final String TAG = "DesktopModeFlagsUtil"; + private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = + "sys.wmshell.desktopmode.dev_toggle_override"; + + // Function called to obtain aconfig flag value. + private final Supplier<Boolean> mFlagFunction; + // Whether the flag state should be affected by developer option. + private final boolean mShouldOverrideByDevOption; + + // Local cache for toggle override, which is initialized once on its first access. It needs to + // be refreshed only on reboots as overridden state takes effect on reboots. + private static ToggleOverride sCachedToggleOverride; + + DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) { + this.mFlagFunction = flagFunction; + this.mShouldOverrideByDevOption = shouldOverrideByDevOption; + } + + /** + * Determines state of flag based on the actual flag and desktop mode developer option + * overrides. + * + * Note: this method makes sure that a constant developer toggle overrides is read until + * reboot. + */ + public boolean isEnabled(Context context) { + if (!Flags.showDesktopWindowingDevOption() + || !mShouldOverrideByDevOption + || context.getContentResolver() == null) { + return mFlagFunction.get(); + } else { + boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode(); + return switch (getToggleOverride(context)) { + case OVERRIDE_UNSET -> mFlagFunction.get(); + // When toggle override matches its default state, don't override flags. This + // helps users reset their feature overrides. + case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get(); + case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true; + }; + } + } + + private ToggleOverride getToggleOverride(Context context) { + // If cached, return it + if (sCachedToggleOverride != null) { + return sCachedToggleOverride; + } + + // Otherwise, fetch and cache it + ToggleOverride override = getToggleOverrideFromSystem(context); + sCachedToggleOverride = override; + Log.d(TAG, "Toggle override initialized to: " + override); + return override; + } + + /** + * Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise + * initializes the system property by reading Settings.Global. + */ + private ToggleOverride getToggleOverrideFromSystem(Context context) { + // A non-persistent System Property is used to store override to ensure it remains + // constant till reboot. + String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null); + ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty); + + // If valid system property, return it + if (overrideFromSystemProperties != null) { + return overrideFromSystemProperties; + } + + // Fallback when System Property is not present (just after reboot) or not valid (user + // manually changed the value): Read from Settings.Global + int settingValue = Settings.Global.getInt( + context.getContentResolver(), + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, + OVERRIDE_UNSET.getSetting() + ); + ToggleOverride overrideFromSettingsGlobal = + ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET); + // Initialize System Property + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue)); + return overrideFromSettingsGlobal; + } + + /** + * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if + * {@code intString} does not correspond to a {@link ToggleOverride}. + */ + private static @Nullable ToggleOverride convertToToggleOverride( + @Nullable String intString + ) { + if (intString == null) return null; + try { + int intValue = Integer.parseInt(intString); + return ToggleOverride.fromSetting(intValue, null); + } catch (NumberFormatException e) { + Log.w(TAG, "Unknown toggleOverride int " + intString); + return null; + } + } + + /** Override state of desktop mode developer option toggle. */ + enum ToggleOverride { + OVERRIDE_UNSET, + OVERRIDE_OFF, + OVERRIDE_ON; + + int getSetting() { + return switch (this) { + case OVERRIDE_ON -> 1; + case OVERRIDE_OFF -> 0; + case OVERRIDE_UNSET -> -1; + }; + } + + static ToggleOverride fromSetting(int setting, @Nullable ToggleOverride fallback) { + return switch (setting) { + case 1 -> OVERRIDE_ON; + case 0 -> OVERRIDE_OFF; + case -1 -> OVERRIDE_UNSET; + default -> fallback; + }; + } + } +} diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index b2a5b02c49e1..f2b4136c51ed 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -59,7 +59,6 @@ android_ravenwood_test { name: "PowerStatsTestsRavenwood", static_libs: [ "services.core", - "platformprotosnano", "coretests-aidl", "ravenwood-junit", "truth", diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 41f1ac72c56d..ea2abf7ddcb8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -141,8 +141,8 @@ import java.util.HashMap; import java.util.List; /** Common base class for window manager unit test classes. */ -class WindowTestsBase extends SystemServiceTestsBase { - final Context mContext = getInstrumentation().getTargetContext(); +public class WindowTestsBase extends SystemServiceTestsBase { + protected final Context mContext = getInstrumentation().getTargetContext(); // Default package name static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo"; diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java new file mode 100644 index 000000000000..e5f2f89ccead --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java @@ -0,0 +1,459 @@ +/* + * 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 com.android.server.wm.utils.DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE; +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_OFF; +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_ON; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; +import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.WindowTestRunner; +import com.android.server.wm.WindowTestsBase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; + +/** + * Test class for [DesktopModeFlagsUtil] + * + * Build/Install/Run: + * atest WmTests:DesktopModeFlagsUtilTest + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DesktopModeFlagsUtilTest extends WindowTestsBase { + + @Rule + public SetFlagsRule setFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + resetCache(); + } + + private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = + "sys.wmshell.desktopmode.dev_toggle_override"; + + @Test + @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_OFF.getSetting()); + // In absence of dev options, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + + @Test + @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_ON.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For overridableFlag, for unset overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For overridableFlag, for unset overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_noOverride_featureFlagOn_returnsTrue() { + setOverride(null); + + // For overridableFlag, in absence of overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noOverride_featureFlagOff_returnsFalse() { + setOverride(null); + + // For overridableFlag, in absence of overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_unrecognizableOverride_featureFlagOn_returnsTrue() { + setOverride(-2); + + // For overridableFlag, for unrecognized overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_unrecognizableOverride_featureFlagOff_returnsFalse() { + setOverride(-2); + + // For overridableFlag, for unrecognizable overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideOff_featureFlagOn_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideOn_featureFlagOff_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + + setOverride(OVERRIDE_ON.getSetting()); + + // Keep overrides constant through the process + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + + setOverride(OVERRIDE_OFF.getSetting()); + + // Keep overrides constant through the process + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(OVERRIDE_ON.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc"); + setOverride(OVERRIDE_OFF.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if currently invalid + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2"); + setOverride(OVERRIDE_OFF.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if currently invalid + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf( + OVERRIDE_OFF.getSetting())); + setOverride(OVERRIDE_ON.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting())); + setOverride(OVERRIDE_OFF.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, + String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + setOverride(OVERRIDE_OFF.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY}) + public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_ON.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_OFF.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + private void setOverride(Integer setting) { + ContentResolver contentResolver = mContext.getContentResolver(); + String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; + + if (setting == null) { + Settings.Global.putString(contentResolver, key, null); + } else { + Settings.Global.putInt(contentResolver, key, setting); + } + } + + private void resetCache() throws Exception { + Field cachedToggleOverride = DesktopModeFlagsUtil.class.getDeclaredField( + "sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(null, null); + + // Clear override cache stored in System property + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + } +} diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index e839fc1ceb0f..7739171b347f 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -137,22 +137,25 @@ bool ParseFeatureFlagsParameter(StringPiece arg, android::IDiagnostics* diag, diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg); return false; } + std::vector<std::string> name_parts = util::Split(flag_name, ':'); if (name_parts.size() > 2) { diag->Error(android::DiagMessage() << "Invalid feature flag and optional value '" << flag_and_value - << "'. Must be in the format 'flag_name[:ro][=true|false]"); + << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]"); return false; } flag_name = name_parts[0]; bool read_only = false; if (name_parts.size() == 2) { - if (name_parts[1] == "ro") { + if (name_parts[1] == "ro" || name_parts[1] == "READ_ONLY") { read_only = true; + } else if (name_parts[1] == "READ_WRITE") { + read_only = false; } else { diag->Error(android::DiagMessage() << "Invalid feature flag and optional value '" << flag_and_value - << "'. Must be in the format 'flag_name[:ro][=true|false]"); + << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]"); return false; } } diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp index 35bc63714e58..78183409ad8f 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -383,7 +383,7 @@ TEST(UtilTest, ParseFeatureFlagsParameter_InvalidValue) { TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) { auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics(); FeatureFlagValues feature_flag_values; - ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar=true,foo:ro=false", diagnostics, + ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar:READ_WRITE=true,foo:ro=false", diagnostics, &feature_flag_values)); EXPECT_THAT( feature_flag_values, @@ -394,11 +394,11 @@ TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) { TEST(UtilTest, ParseFeatureFlagsParameter_Valid) { auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics(); FeatureFlagValues feature_flag_values; - ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar:ro =FALSE,baz=, quux", diagnostics, - &feature_flag_values)); + ASSERT_TRUE(ParseFeatureFlagsParameter("foo:READ_ONLY= true, bar:ro =FALSE,baz:READ_WRITE=, quux", + diagnostics, &feature_flag_values)); EXPECT_THAT( feature_flag_values, - UnorderedElementsAre(Pair("foo", FeatureFlagProperties{false, std::optional<bool>(true)}), + UnorderedElementsAre(Pair("foo", FeatureFlagProperties{true, std::optional<bool>(true)}), Pair("bar", FeatureFlagProperties{true, std::optional<bool>(false)}), Pair("baz", FeatureFlagProperties{false, std::nullopt}), Pair("quux", FeatureFlagProperties{false, std::nullopt}))); |