Merge "Support force-stop IME package for background users" into main
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 1d950dc..6343313 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -119,9 +119,11 @@
@Override
public boolean applyLocalVisibilityOverride() {
- ImeTracing.getInstance().triggerClientDump(
- "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
- mController.getHost().getInputMethodManager(), null /* icProto */);
+ if (!Flags.refactorInsetsController()) {
+ ImeTracing.getInstance().triggerClientDump(
+ "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+ mController.getHost().getInputMethodManager(), null /* icProto */);
+ }
return super.applyLocalVisibilityOverride();
}
@@ -205,9 +207,13 @@
@Override
public void removeSurface() {
- final IBinder window = mController.getHost().getWindowToken();
- if (window != null) {
- getImm().removeImeSurface(window);
+ if (Flags.refactorInsetsController()) {
+ super.removeSurface();
+ } else {
+ final IBinder window = mController.getHost().getWindowToken();
+ if (window != null) {
+ getImm().removeImeSurface(window);
+ }
}
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index df2af73..f166b89 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -765,7 +765,7 @@
public InsetsController(Host host) {
this(host, (controller, id, type) -> {
- if (type == ime()) {
+ if (!Flags.refactorInsetsController() && type == ime()) {
return new ImeInsetsSourceConsumer(id, controller.mState,
Transaction::new, controller);
} else {
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index c73cbc6..477e35b 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -43,6 +43,7 @@
import android.view.inputmethod.ImeTracker;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.ImeTracing;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -296,6 +297,13 @@
@VisibleForTesting(visibility = PACKAGE)
public boolean applyLocalVisibilityOverride() {
+ if (Flags.refactorInsetsController()) {
+ if (mType == WindowInsets.Type.ime()) {
+ ImeTracing.getInstance().triggerClientDump(
+ "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+ mController.getHost().getInputMethodManager(), null /* icProto */);
+ }
+ }
final InsetsSource source = mState.peekSource(mId);
if (source == null) {
return false;
@@ -396,6 +404,14 @@
*/
public void removeSurface() {
// no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
+ if (Flags.refactorInsetsController()) {
+ if (mType == WindowInsets.Type.ime()) {
+ final IBinder window = mController.getHost().getWindowToken();
+ if (window != null) {
+ mController.getHost().getInputMethodManager().removeImeSurface(window);
+ }
+ }
+ }
}
@VisibleForTesting(visibility = PACKAGE)
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index c9d2eec..fed8eea 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1352,12 +1352,16 @@
case MSG_SET_VISIBILITY:
final boolean visible = msg.arg1 != 0;
synchronized (mH) {
- if (visible) {
- showSoftInput(mServedView, /* flags */ 0);
- } else {
- if (mCurRootView != null
- && mCurRootView.getInsetsController() != null) {
- mCurRootView.getInsetsController().hide(WindowInsets.Type.ime());
+ if (mCurRootView != null) {
+ final var insetsController = mCurRootView.getInsetsController();
+ if (insetsController != null) {
+ if (visible) {
+ insetsController.show(WindowInsets.Type.ime(),
+ false /* fromIme */, null /* statsToken */);
+ } else {
+ insetsController.hide(WindowInsets.Type.ime(),
+ false /* fromIme */, null /* statsToken */);
+ }
}
}
}
@@ -2334,16 +2338,18 @@
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
if (Flags.refactorInsetsController()) {
+ final var viewRootImpl = view.getViewRootImpl();
// In case of a running show IME animation, it should not be requested visible,
// otherwise the animation would jump and not be controlled by the user anymore
- if ((mCurRootView.getInsetsController().computeUserAnimatingTypes()
- & WindowInsets.Type.ime()) == 0) {
+ if (viewRootImpl != null
+ && (viewRootImpl.getInsetsController().computeUserAnimatingTypes()
+ & WindowInsets.Type.ime()) == 0) {
// TODO(b/322992891) handle case of SHOW_IMPLICIT
- view.getWindowInsetsController().show(WindowInsets.Type.ime());
+ viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(),
+ false /* fromIme */, statsToken);
return true;
- } else {
- return false;
}
+ return false;
} else {
// Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
// TODO(b/229426865): call WindowInsetsController#show instead.
@@ -2497,7 +2503,10 @@
if (Flags.refactorInsetsController()) {
// TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY
- servedView.getWindowInsetsController().hide(WindowInsets.Type.ime());
+ final var viewRootImpl = servedView.getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime());
+ }
return true;
} else {
return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java
index 6629fdc4..16727c3 100644
--- a/core/java/android/webkit/WebViewProviderInfo.java
+++ b/core/java/android/webkit/WebViewProviderInfo.java
@@ -23,6 +23,9 @@
import android.os.Parcelable;
import android.util.Base64;
+import java.util.Arrays;
+import java.util.Objects;
+
/**
* @hide
*/
@@ -80,6 +83,35 @@
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 0000000..1fb5f2c
--- /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 0000000..9128dca
--- /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
similarity index 73%
rename from services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
rename to core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
index 62efbc3..ac1f7d0 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.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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
.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/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 47b28899..248db65 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -37,8 +37,12 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.view.WindowManager.BadTokenException;
import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
import android.widget.TextView;
@@ -46,6 +50,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -61,6 +66,9 @@
@RunWith(AndroidJUnit4.class)
public class ImeInsetsSourceConsumerTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
InsetsSourceConsumer mImeConsumer;
@Spy InsetsController mController;
@@ -112,6 +120,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testImeRequestedVisibleAwaitingControl() {
// Set null control and then request show.
mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -141,6 +150,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testImeRequestedVisibleAwaitingLeash() {
// Set null control, then request show.
mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -185,6 +195,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testImeGetAndClearSkipAnimationOnce_expectSkip() {
// Expect IME animation will skipped when the IME is visible at first place.
verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */,
@@ -192,6 +203,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() {
// Expect IME animation will not skipped if previously no view focused when gained the
// window focus and requesting the IME visible next time.
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 9bcd9b0..99bd96e 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 @@
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 db962e7..2406bde 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 @@
@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 7886e85..49b974f 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.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 @@
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 @@
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 @@
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 b2fcb5f..0ff7f84 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 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 @@
/** 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 @@
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 7b994d5..2f7cdd6 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 @@
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 a17076b..4e01a71 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -539,6 +539,7 @@
"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 97a45fb..035e2fb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -870,6 +870,13 @@
}
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/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
index 5e87f46..61873ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.statusbar.notification.row.ui.viewmodel
+import android.app.Notification
import android.app.PendingIntent
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -90,7 +91,8 @@
name: String = "example",
timeRemaining: Duration = Duration.ofMinutes(3),
resumeIntent: PendingIntent? = null,
- resetIntent: PendingIntent? = null
+ addMinuteAction: Notification.Action? = null,
+ resetAction: Notification.Action? = null
) =
TimerContentModel(
icon = icon,
@@ -99,7 +101,8 @@
Paused(
timeRemaining = timeRemaining,
resumeIntent = resumeIntent,
- resetIntent = resetIntent,
+ addMinuteAction = addMinuteAction,
+ resetAction = resetAction,
)
)
}
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 0000000..fdfc7f1
--- /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/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
index f2bfbe5c9..3a679e3 100644
--- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
+++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
@@ -33,7 +33,6 @@
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
- android:src="@drawable/ic_close"
app:tint="@android:color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/label"
@@ -88,11 +87,10 @@
/>
<com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+ style="@*android:style/NotificationEmphasizedAction"
android:id="@+id/mainButton"
android:layout_width="124dp"
android:layout_height="wrap_content"
- tools:text="Reset"
- tools:drawableStart="@android:drawable/ic_menu_add"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/altButton"
app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
@@ -101,15 +99,23 @@
/>
<com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+ style="@*android:style/NotificationEmphasizedAction"
android:id="@+id/altButton"
- tools:text="Reset"
- tools:drawableStart="@android:drawable/ic_menu_add"
- android:drawablePadding="2dp"
- android:drawableTint="@android:color/white"
android:layout_width="124dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
app:layout_constraintStart_toEndOf="@id/mainButton"
+ app:layout_constraintEnd_toEndOf="@id/resetButton"
+ android:paddingEnd="4dp"
+ />
+
+ <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+ style="@*android:style/NotificationEmphasizedAction"
+ android:id="@+id/resetButton"
+ android:layout_width="124dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+ app:layout_constraintStart_toEndOf="@id/altButton"
app:layout_constraintEnd_toEndOf="parent"
android:paddingEnd="4dp"
/>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index be28504..8322b6c 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 af7ecf6..1ba274f 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.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 @@
// 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 8af5665..ee709c4 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.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 0000000..664d496
--- /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 4f6a64f..bd08685 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 @@
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/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
index b8af369..fe86375 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -122,12 +122,15 @@
val timeRemaining = parseTimeDelta(remaining)
TimerContentModel(
icon = icon,
- name = total,
+ // TODO: b/352142761 - define and use a string resource rather than " Timer".
+ // (The UX isn't final so using " Timer" for now).
+ name = total.replace("Σ", "") + " Timer",
state =
TimerContentModel.TimerState.Paused(
timeRemaining = timeRemaining,
- resumeIntent = notification.findActionWithName("Resume"),
- resetIntent = notification.findActionWithName("Reset"),
+ resumeIntent = notification.findStartIntent(),
+ addMinuteAction = notification.findAddMinuteAction(),
+ resetAction = notification.findResetAction(),
)
)
}
@@ -136,12 +139,15 @@
val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
TimerContentModel(
icon = icon,
- name = total,
+ // TODO: b/352142761 - define and use a string resource rather than " Timer".
+ // (The UX isn't final so using " Timer" for now).
+ name = total.replace("Σ", "") + " Timer",
state =
TimerContentModel.TimerState.Running(
finishTime = finishTime,
- pauseIntent = notification.findActionWithName("Pause"),
- addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
+ pauseIntent = notification.findPauseIntent(),
+ addMinuteAction = notification.findAddMinuteAction(),
+ resetAction = notification.findResetAction(),
)
)
}
@@ -149,8 +155,34 @@
}
}
- private fun Notification.findActionWithName(name: String): PendingIntent? {
- return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
+ private fun Notification.findPauseIntent(): PendingIntent? {
+ return actions
+ .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true }
+ ?.actionIntent
+ }
+
+ private fun Notification.findStartIntent(): PendingIntent? {
+ return actions
+ .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true }
+ ?.actionIntent
+ }
+
+ // TODO: b/352142761 - switch to system attributes for label and icon.
+ // - We probably want a consistent look for the Reset button. (Double check with UX.)
+ // - Using the custom assets now since I couldn't an existing "Reset" icon.
+ private fun Notification.findResetAction(): Notification.Action? {
+ return actions.firstOrNull {
+ it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true
+ }
+ }
+
+ // TODO: b/352142761 - check with UX on whether this should be required.
+ // - Alternative is to allow for optional actions in addition to main and reset.
+ // - For optional actions, we should take the custom label and icon.
+ private fun Notification.findAddMinuteAction(): Notification.Action? {
+ return actions.firstOrNull {
+ it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true
+ }
}
private fun parseCurrentTime(current: String): Long {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
index 5584701..33b2564 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.row.shared
+import android.app.Notification
import android.app.PendingIntent
import java.time.Duration
@@ -32,6 +33,9 @@
) : RichOngoingContentModel {
/** The state (paused or running) of the timer, and relevant time */
sealed interface TimerState {
+ val addMinuteAction: Notification.Action?
+ val resetAction: Notification.Action?
+
/**
* Indicates a running timer
*
@@ -41,7 +45,8 @@
data class Running(
val finishTime: Long,
val pauseIntent: PendingIntent?,
- val addOneMinuteIntent: PendingIntent?,
+ override val addMinuteAction: Notification.Action?,
+ override val resetAction: Notification.Action?,
) : TimerState
/**
@@ -53,7 +58,8 @@
data class Paused(
val timeRemaining: Duration,
val resumeIntent: PendingIntent?,
- val resetIntent: PendingIntent?,
+ override val addMinuteAction: Notification.Action?,
+ override val resetAction: Notification.Action?,
) : TimerState
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
index 0d83ace..8c95187 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
@@ -18,8 +18,9 @@
import android.annotation.DrawableRes
import android.content.Context
+import android.graphics.BlendMode
import android.util.AttributeSet
-import android.widget.Button
+import com.android.internal.widget.EmphasizedNotificationButton
class TimerButtonView
@JvmOverloads
@@ -28,14 +29,19 @@
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
-) : Button(context, attrs, defStyleAttr, defStyleRes) {
+) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) {
private val Int.dp: Int
get() = (this * context.resources.displayMetrics.density).toInt()
fun setIcon(@DrawableRes icon: Int) {
val drawable = context.getDrawable(icon)
+
+ drawable?.mutate()
+ drawable?.setTintList(textColors)
+ drawable?.setTintBlendMode(BlendMode.SRC_IN)
drawable?.setBounds(0, 0, 24.dp, 24.dp)
+
setCompoundDrawablesRelative(drawable, null, null, null)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
index 2e164d6..d481b50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.notification.row.ui.view
import android.content.Context
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
import android.os.SystemClock
import android.util.AttributeSet
import android.widget.Chronometer
@@ -48,6 +48,9 @@
lateinit var altButton: TimerButtonView
private set
+ lateinit var resetButton: TimerButtonView
+ private set
+
override fun onFinishInflate() {
super.onFinishInflate()
icon = requireViewById(R.id.icon)
@@ -56,13 +59,14 @@
pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
mainButton = requireViewById(R.id.mainButton)
altButton = requireViewById(R.id.altButton)
+ resetButton = requireViewById(R.id.resetButton)
}
/** the resources configuration has changed such that the view needs to be reinflated */
fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
- fun setIcon(iconDrawable: Drawable?) {
- this.icon.setImageDrawable(iconDrawable)
+ fun setIcon(icon: Icon?) {
+ this.icon.setImageIcon(icon)
}
fun setLabel(label: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
index c9ff589..042d1bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.row.ui.viewbinder
+import android.content.res.ColorStateList
+import android.graphics.drawable.Icon
import android.view.View
import androidx.core.view.isGone
import androidx.lifecycle.lifecycleScope
@@ -46,12 +48,43 @@
launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
+ launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } }
}
fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
if (model != null) {
- buttonView.setIcon(model.iconRes)
- buttonView.setText(model.labelRes)
+ buttonView.setButtonBackground(
+ ColorStateList.valueOf(
+ buttonView.context.getColor(com.android.internal.R.color.system_accent2_100)
+ )
+ )
+ buttonView.setTextColor(
+ buttonView.context.getColor(
+ com.android.internal.R.color.notification_primary_text_color_light
+ )
+ )
+
+ when (model) {
+ is TimerViewModel.ButtonViewModel.WithSystemAttrs -> {
+ buttonView.setIcon(model.iconRes)
+ buttonView.setText(model.labelRes)
+ }
+ is TimerViewModel.ButtonViewModel.WithCustomAttrs -> {
+ // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons
+ // with empty resPackage? RemoteViews handles this by using a different
+ // `contextForResources` for inflation.
+ val icon =
+ if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "")
+ Icon.createWithResource(
+ "com.google.android.deskclock",
+ model.icon.resId
+ )
+ else model.icon
+ buttonView.setImageIcon(icon)
+ buttonView.text = model.label
+ }
+ }
+
buttonView.setOnClickListener(
model.pendingIntent?.let { pendingIntent ->
View.OnClickListener { pendingIntent.send() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
index a85c87f..768a093 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
@@ -19,7 +19,7 @@
import android.annotation.DrawableRes
import android.annotation.StringRes
import android.app.PendingIntent
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
@@ -44,7 +44,7 @@
private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
- val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable }
+ val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon }
val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
@@ -57,13 +57,13 @@
state.map {
when (it) {
is TimerState.Paused ->
- ButtonViewModel(
+ ButtonViewModel.WithSystemAttrs(
it.resumeIntent,
com.android.systemui.res.R.string.controls_media_resume, // "Resume",
com.android.systemui.res.R.drawable.ic_media_play
)
is TimerState.Running ->
- ButtonViewModel(
+ ButtonViewModel.WithSystemAttrs(
it.pauseIntent,
com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
com.android.systemui.res.R.drawable.ic_media_pause
@@ -73,31 +73,41 @@
val altButtonModel: Flow<ButtonViewModel?> =
state.map {
- when (it) {
- is TimerState.Paused ->
- it.resetIntent?.let { resetIntent ->
- ButtonViewModel(
- resetIntent,
- com.android.systemui.res.R.string.reset, // "Reset",
- com.android.systemui.res.R.drawable.ic_close_white_rounded
- )
- }
- is TimerState.Running ->
- it.addOneMinuteIntent?.let { addOneMinuteIntent ->
- ButtonViewModel(
- addOneMinuteIntent,
- com.android.systemui.res.R.string.add, // "Add 1 minute",
- com.android.systemui.res.R.drawable.ic_add
- )
- }
+ it.addMinuteAction?.let { action ->
+ ButtonViewModel.WithCustomAttrs(
+ action.actionIntent,
+ action.title, // "1:00",
+ action.getIcon()
+ )
}
}
- data class ButtonViewModel(
- val pendingIntent: PendingIntent?,
- @StringRes val labelRes: Int,
- @DrawableRes val iconRes: Int,
- )
+ val resetButtonModel: Flow<ButtonViewModel?> =
+ state.map {
+ it.resetAction?.let { action ->
+ ButtonViewModel.WithCustomAttrs(
+ action.actionIntent,
+ action.title, // "Reset",
+ action.getIcon()
+ )
+ }
+ }
+
+ sealed interface ButtonViewModel {
+ val pendingIntent: PendingIntent?
+
+ data class WithSystemAttrs(
+ override val pendingIntent: PendingIntent?,
+ @StringRes val labelRes: Int,
+ @DrawableRes val iconRes: Int,
+ ) : ButtonViewModel
+
+ data class WithCustomAttrs(
+ override val pendingIntent: PendingIntent?,
+ val label: CharSequence,
+ val icon: Icon,
+ ) : ButtonViewModel
+ }
}
private fun Duration.format(): String {
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 e4d0668..7a521a6 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 @@
* 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 @@
}
}
.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 6db1eac..2b094d6 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.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 @@
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 0000000..91bfdff
--- /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 0000000..73d361f6
--- /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 0000000..5bd26cc
--- /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 0000000..e84c8b6
--- /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 49e3f04..31f93b4 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 @@
@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 0000000..db95fad
--- /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 0000000..ddc66c7
--- /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 0000000..ec8e907
--- /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 0000000..d952d07
--- /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 49e793f..4357f2b 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.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 @@
}
}
+ /**
+ * @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 @@
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 @@
}
}
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 d7da2f0..a5ec2ba 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 @@
+ 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 @@
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 @@
// 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 092ee16..67985ef 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 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 @@
.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 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 @@
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 @@
return StatsManager.PULL_SUCCESS;
}
- private boolean addStatsForPredefinedComponent(
+ private static boolean addStatsForPredefinedComponent(
List<StatsEvent> data,
SessionInfo sessionInfo,
int uid,
@@ -1420,7 +1380,7 @@
powerComponentDurationMillis);
}
- private boolean addStatsForCustomComponent(
+ private static boolean addStatsForCustomComponent(
List<StatsEvent> data,
SessionInfo sessionInfo,
int uid,
@@ -1462,7 +1422,7 @@
* 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 @@
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/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index b346767..ecbbd46 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1441,9 +1441,9 @@
(windowToken, imeVisible) -> {
if (Flags.refactorInsetsController()) {
if (imeVisible) {
- showSoftInputInternal(windowToken);
+ showCurrentInputInternal(windowToken);
} else {
- hideSoftInputInternal(windowToken);
+ hideCurrentInputInternal(windowToken);
}
}
});
@@ -1919,7 +1919,7 @@
if (Flags.refactorInsetsController()) {
if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null
&& userData.mImeBindingState.mFocusedWindow != null) {
- showSoftInputInternal(userData.mImeBindingState.mFocusedWindow);
+ showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow);
}
} else {
if (isShowRequestedForCurrentWindow(userId)) {
@@ -3151,8 +3151,8 @@
}
}
- boolean showSoftInputInternal(IBinder windowToken) {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal");
+ boolean showCurrentInputInternal(IBinder windowToken) {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal");
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#showSoftInput", mDumper);
synchronized (ImfLock.class) {
@@ -3171,8 +3171,8 @@
}
}
- boolean hideSoftInputInternal(IBinder windowToken) {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal");
+ boolean hideCurrentInputInternal(IBinder windowToken) {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal");
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#hideSoftInput", mDumper);
synchronized (ImfLock.class) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index 1f341147..e0c0c2c 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 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 @@
"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 @@
* 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/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 3483842..dcadb0f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -397,9 +397,11 @@
onRequestedVisibleTypesChanged(newControlTargets.valueAt(i));
}
newControlTargets.clear();
- // Check for and try to run the scheduled show IME request (if it exists), as we
- // now applied the surface transaction and notified the target of the new control.
- getImeSourceProvider().checkAndStartShowImePostLayout();
+ if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+ // Check for and try to run the scheduled show IME request (if it exists), as we
+ // now applied the surface transaction and notified the target of the new control.
+ getImeSourceProvider().checkAndStartShowImePostLayout();
+ }
});
}
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 0000000..4211764
--- /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 b2a5b02..f2b4136 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -59,7 +59,6 @@
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 41f1ac7..ea2abf7 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.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 0000000..e5f2f89
--- /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 e839fc1..7739171 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -137,22 +137,25 @@
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 35bc637..7818340 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -383,7 +383,7 @@
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_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})));