diff options
95 files changed, 3501 insertions, 470 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 252d23f69400..ee9c64f97382 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -292,13 +292,15 @@ import java.util.function.Consumer; * to the user, it must be completely restarted and restored to its previous state.</li> * </ul> * - * <p>The following diagram shows the important state paths of an Activity. + * <p>The following diagram shows the important state paths of an activity. * The square rectangles represent callback methods you can implement to - * perform operations when the Activity moves between states. The colored - * ovals are major states the Activity can be in.</p> + * perform operations when the activity moves between states. The colored + * ovals are major states the activity can be in.</p> * - * <p><img src="../../../images/activity_lifecycle.png" - * alt="State diagram for an Android Activity Lifecycle." border="0" /></p> + * <p><img class="invert" + * style="display: block; margin: auto;" + * src="../../../images/activity_lifecycle.png" + * alt="State diagram for the Android activity lifecycle." /></p> * * <p>There are three key loops you may be interested in monitoring within your * activity: @@ -505,7 +507,7 @@ import java.util.function.Consumer; * changes.</p> * * <p>Unless you specify otherwise, a configuration change (such as a change - * in screen orientation, language, input devices, etc) will cause your + * in screen orientation, language, input devices, etc.) will cause your * current activity to be <em>destroyed</em>, going through the normal activity * lifecycle process of {@link #onPause}, * {@link #onStop}, and {@link #onDestroy} as appropriate. If the activity @@ -1838,7 +1840,7 @@ public class Activity extends ContextThemeWrapper * * <p>You can call {@link #finish} from within this function, in * which case onDestroy() will be immediately called after {@link #onCreate} without any of the - * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc) + * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc.) * executing. * * <p><em>Derived classes must call through to the super class's @@ -2132,7 +2134,7 @@ public class Activity extends ContextThemeWrapper * * <p>You can call {@link #finish} from within this function, in * which case {@link #onStop} will be immediately called after {@link #onStart} without the - * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc) executing. + * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc.) executing. * * <p><em>Derived classes must call through to the super class's * implementation of this method. If they do not, an exception will be diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index bb4f556532f7..8e6b88c66408 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -83,6 +83,13 @@ flag { } flag { + name: "modes_cleanup_implicit" + namespace: "systemui" + description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI" + bug: "394087495" +} + +flag { name: "api_tvextender" is_exported: true namespace: "systemui" diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index fcdb02ab5da2..ba1473cf5ed7 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -120,6 +120,16 @@ flag { } flag { + name: "correct_virtual_display_power_state" + namespace: "virtual_devices" + description: "Fix the virtual display power state" + bug: "371125136" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "vdm_settings" namespace: "virtual_devices" description: "Show virtual devices in Settings" diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 40c532498fbc..36fa05905814 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -29,6 +29,8 @@ import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.TypedValue; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.pm.pkg.component.AconfigFlags; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.internal.util.XmlUtils; import dalvik.annotation.optimization.CriticalNative; @@ -50,6 +52,7 @@ import java.io.Reader; @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class XmlBlock implements AutoCloseable { private static final boolean DEBUG=false; + public static final String ANDROID_RESOURCES = "http://schemas.android.com/apk/res/android"; @UnsupportedAppUsage public XmlBlock(byte[] data) { @@ -343,6 +346,23 @@ public final class XmlBlock implements AutoCloseable { if (ev == ERROR_BAD_DOCUMENT) { throw new XmlPullParserException("Corrupt XML binary file"); } + if (useLayoutReadwrite() && ev == START_TAG) { + AconfigFlags flags = ParsingPackageUtils.getAconfigFlags(); + if (flags.skipCurrentElement(/* pkg= */ null, this)) { + int depth = 1; + while (depth > 0) { + int ev2 = nativeNext(mParseState); + if (ev2 == ERROR_BAD_DOCUMENT) { + throw new XmlPullParserException("Corrupt XML binary file"); + } else if (ev2 == START_TAG) { + depth++; + } else if (ev2 == END_TAG) { + depth--; + } + } + return next(); + } + } if (mDecNextDepth) { mDepth--; mDecNextDepth = false; @@ -368,6 +388,18 @@ public final class XmlBlock implements AutoCloseable { } return ev; } + + // Until ravenwood supports AconfigFlags, we just don't do layoutReadwriteFlags(). + @android.ravenwood.annotation.RavenwoodReplace( + bug = 396458006, blockedBy = AconfigFlags.class) + private static boolean useLayoutReadwrite() { + return Flags.layoutReadwriteFlags(); + } + + private static boolean useLayoutReadwrite$ravenwood() { + return false; + } + public void require(int type, String namespace, String name) throws XmlPullParserException,IOException { if (type != getEventType() || (namespace != null && !namespace.equals( getNamespace () ) ) diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 4011574da879..6f94c1b2d274 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -309,6 +309,7 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin"; private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects"; private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride"; + private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation"; private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale"; private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY = @@ -1187,11 +1188,7 @@ public class ZenModeConfig implements Parcelable { rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); - Long deletionInstant = tryParseLong( - parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); - if (deletionInstant != null) { - rt.deletionInstant = Instant.ofEpochMilli(deletionInstant); - } + rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null); if (Flags.modesUi()) { rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN, ORIGIN_UNKNOWN); @@ -1199,6 +1196,9 @@ public class ZenModeConfig implements Parcelable { RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0); rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE, ZenRule.OVERRIDE_NONE); + if (Flags.modesCleanupImplicit()) { + rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null); + } } return rt; @@ -1249,10 +1249,7 @@ public class ZenModeConfig implements Parcelable { out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, rule.zenDeviceEffectsUserModifiedFields); - if (rule.deletionInstant != null) { - out.attributeLong(null, RULE_ATT_DELETION_INSTANT, - rule.deletionInstant.toEpochMilli()); - } + writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant); if (Flags.modesUi()) { out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin); out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, @@ -1260,6 +1257,16 @@ public class ZenModeConfig implements Parcelable { if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) { out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride); } + if (Flags.modesCleanupImplicit()) { + writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation); + } + } + } + + private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att, + @Nullable Instant instant) throws IOException { + if (instant != null) { + out.attributeLong(null, att, instant.toEpochMilli()); } } @@ -1600,6 +1607,19 @@ public class ZenModeConfig implements Parcelable { return values; } + @Nullable + private static Instant safeInstant(TypedXmlPullParser parser, String att, + @Nullable Instant defValue) { + final String strValue = parser.getAttributeValue(null, att); + if (!TextUtils.isEmpty(strValue)) { + Long longValue = tryParseLong(strValue, null); + if (longValue != null) { + return Instant.ofEpochMilli(longValue); + } + } + return defValue; + } + @Override public int describeContents() { return 0; @@ -2598,6 +2618,18 @@ public class ZenModeConfig implements Parcelable { @ConditionOverride int conditionOverride = OVERRIDE_NONE; + /** + * Last time at which the rule was activated (for any reason, including overrides). + * If {@code null}, the rule has never been activated since its creation. + * + * <p>Note that this was previously untracked, so it will also be {@code null} for rules + * created before we started tracking and never activated since -- make sure to account for + * it, for example by falling back to {@link #creationTime} in logic involving this field. + */ + @Nullable + @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT) + public Instant lastActivation; + public ZenRule() { } public ZenRule(Parcel source) { @@ -2635,24 +2667,29 @@ public class ZenModeConfig implements Parcelable { disabledOrigin = source.readInt(); legacySuppressedEffects = source.readInt(); conditionOverride = source.readInt(); + if (Flags.modesCleanupImplicit()) { + if (source.readInt() == 1) { + lastActivation = Instant.ofEpochMilli(source.readLong()); + } + } } } /** - * Whether this ZenRule can be updated by an app. In general, rules that have been - * customized by the user cannot be further updated by an app, with some exceptions: + * Whether this ZenRule has been customized by the user in any way. + + * <p>In general, rules that have been customized by the user cannot be further updated by + * an app, with some exceptions: * <ul> * <li>Non user-configurable fields, like type, icon, configurationActivity, etc. * <li>Name, if the name was not specifically modified by the user (to support language * switches). * </ul> */ - public boolean canBeUpdatedByApp() { - // The rule is considered updateable if its bitmask has no user modifications, and - // the bitmasks of the policy and device effects have no modification. - return userModifiedFields == 0 - && zenPolicyUserModifiedFields == 0 - && zenDeviceEffectsUserModifiedFields == 0; + public boolean isUserModified() { + return userModifiedFields != 0 + || zenPolicyUserModifiedFields != 0 + || zenDeviceEffectsUserModifiedFields != 0; } @Override @@ -2708,6 +2745,14 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(disabledOrigin); dest.writeInt(legacySuppressedEffects); dest.writeInt(conditionOverride); + if (Flags.modesCleanupImplicit()) { + if (lastActivation != null) { + dest.writeInt(1); + dest.writeLong(lastActivation.toEpochMilli()); + } else { + dest.writeInt(0); + } + } } } @@ -2760,6 +2805,9 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { sb.append(",disabledOrigin=").append(disabledOrigin); sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects); + if (Flags.modesCleanupImplicit()) { + sb.append(",lastActivation=").append(lastActivation); + } } return sb.append(']').toString(); @@ -2838,6 +2886,10 @@ public class ZenModeConfig implements Parcelable { && other.disabledOrigin == disabledOrigin && other.legacySuppressedEffects == legacySuppressedEffects && other.conditionOverride == conditionOverride; + if (Flags.modesCleanupImplicit()) { + finalEquals = finalEquals + && Objects.equals(other.lastActivation, lastActivation); + } } return finalEquals; @@ -2846,13 +2898,23 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { if (Flags.modesUi()) { - return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, - component, configurationActivity, pkg, id, enabler, zenPolicy, - zenDeviceEffects, allowManualInvocation, iconResName, - triggerDescription, type, userModifiedFields, - zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, - deletionInstant, disabledOrigin, legacySuppressedEffects, - conditionOverride); + if (Flags.modesCleanupImplicit()) { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride, lastActivation); + } else { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride); + } } else { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1a311d572e0b..2188469bdf03 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2643,6 +2643,15 @@ <!-- MMS user agent prolfile url --> <string name="config_mms_user_agent_profile_url" translatable="false"></string> + <!-- The default list of possible CMF Names|Style|ColorSource. This array can be + overridden device-specific resources. A wildcard (fallback) must be supplied. + Name - Read from `ro.boot.hardware.color` sysprop. Fallback (*) required. + Styles - frameworks/libs/systemui/monet/src/com/android/systemui/monet/Style.java + Color - `home_wallpaper` (for color extraction) or a hexadecimal int (#FFcc99) --> + <string-array name="theming_defaults"> + <item>*|TONAL_SPOT|home_wallpaper</item> + </string-array> + <!-- National Language Identifier codes for the following two config items. (from 3GPP TS 23.038 V9.1.1 Table 6.2.1.2.4.1): 0 - reserved diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 4974285d3a35..ffcfce9c420e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5903,6 +5903,9 @@ <java-symbol type="drawable" name="ic_notification_summarization" /> <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" /> + <!-- Device CMF Theming Settings --> + <java-symbol type="array" name="theming_defaults" /> + <!-- Advanced Protection Service USB feature --> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" /> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java new file mode 100644 index 000000000000..2e389b7dd151 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 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.wm.shell.pip2.phone; + +import static android.view.MotionEvent.ACTION_BUTTON_PRESS; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PipTouchStateTest extends ShellTestCase { + + private PipTouchState mTouchState; + private CountDownLatch mDoubleTapCallbackTriggeredLatch; + private CountDownLatch mHoverExitCallbackTriggeredLatch; + private TestShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + mMainExecutor = new TestShellExecutor(); + mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1); + mHoverExitCallbackTriggeredLatch = new CountDownLatch(1); + mTouchState = new PipTouchState(ViewConfiguration.get(getContext()), + mDoubleTapCallbackTriggeredLatch::countDown, + mHoverExitCallbackTriggeredLatch::countDown, + mMainExecutor); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + } + + @Test + public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertTrue(mTouchState.isWaitingForDoubleTap()); + + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10); + mTouchState.scheduleDoubleTapTimeoutCallback(); + + mMainExecutor.flushAll(); + assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testDoubleTapDrag_doubleTapCanceled() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500)); + assertTrue(mTouchState.isDragging()); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTap_doubleTapRegistered() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertTrue(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(), + 0, 0)); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) { + return MotionEvent.obtain(0, eventTime, action, x, y, 0); + } + +} diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp index ea9e9a2d4280..9aacdcb9ca92 100644 --- a/libs/androidfw/LocaleDataLookup.cpp +++ b/libs/androidfw/LocaleDataLookup.cpp @@ -14871,12 +14871,22 @@ static uint32_t findLatnParent(uint32_t packed_lang_region) { case 0x656E4154u: // en-AT -> en-150 case 0x656E4245u: // en-BE -> en-150 case 0x656E4348u: // en-CH -> en-150 + case 0x656E435Au: // en-CZ -> en-150 case 0x656E4445u: // en-DE -> en-150 case 0x656E444Bu: // en-DK -> en-150 + case 0x656E4553u: // en-ES -> en-150 case 0x656E4649u: // en-FI -> en-150 + case 0x656E4652u: // en-FR -> en-150 + case 0x656E4855u: // en-HU -> en-150 + case 0x656E4954u: // en-IT -> en-150 case 0x656E4E4Cu: // en-NL -> en-150 + case 0x656E4E4Fu: // en-NO -> en-150 + case 0x656E504Cu: // en-PL -> en-150 + case 0x656E5054u: // en-PT -> en-150 + case 0x656E524Fu: // en-RO -> en-150 case 0x656E5345u: // en-SE -> en-150 case 0x656E5349u: // en-SI -> en-150 + case 0x656E534Bu: // en-SK -> en-150 return 0x656E80A1u; case 0x65734152u: // es-AR -> es-419 case 0x6573424Fu: // es-BO -> es-419 diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 3e241bfe6447..099d6b645a6d 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -974,6 +974,16 @@ flag { } flag { + name: "use_notif_inflation_thread_for_footer" + namespace: "systemui" + description: "use the @NotifInflation thread for FooterView and EmptyShadeView inflation" + bug: "375320642" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notify_power_manager_user_activity_background" namespace: "systemui" description: "Decide whether to notify the user activity to power manager in the background thread." @@ -1977,6 +1987,16 @@ flag { } flag { + name: "hardware_color_styles" + namespace: "systemui" + description: "Enables loading initial colors based ion hardware color" + bug: "347286986" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "shade_launch_accessibility" namespace: "systemui" description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events." diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt index 1a3606e413cc..da25bcac6c95 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -44,6 +45,7 @@ import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -65,7 +67,7 @@ class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestC private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val communalSceneRepository = kosmos.fakeCommunalSceneRepository - private val sceneInteractor = kosmos.sceneInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val underTest: CommunalTransitionViewModel by lazy { kosmos.communalTransitionViewModel diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 329627af8ec2..e36d2455d316 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -61,6 +61,7 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.eq import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -73,6 +74,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { @@ -80,21 +82,26 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { private val testScope: TestScope = kosmos.testScope private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor + private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val fakeUserRepository = kosmos.fakeUserRepository private val facePropertyRepository = kosmos.facePropertyRepository - private val fakeDeviceEntryFingerprintAuthInteractor = - kosmos.deviceEntryFingerprintAuthInteractor - private val powerInteractor = kosmos.powerInteractor private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository - private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor + private val keyguardUpdateMonitor by lazy { kosmos.keyguardUpdateMonitor } private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig private val trustManager = kosmos.trustManager - private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor + + private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } + private val fakeDeviceEntryFingerprintAuthInteractor by lazy { + kosmos.deviceEntryFingerprintAuthInteractor + } + private val powerInteractor by lazy { kosmos.powerInteractor } + private val deviceEntryFaceAuthStatusInteractor by lazy { + kosmos.deviceEntryFaceAuthStatusInteractor + } @Before fun setup() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 29e95cd911f8..0b42898d82ae 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState @@ -46,20 +47,31 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardTransitionInteractorTest : SysuiTestCase() { - val kosmos = testKosmos() - val underTest = kosmos.keyguardTransitionInteractor - val repository = kosmos.fakeKeyguardTransitionRepository - val testScope = kosmos.testScope + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var underTest: KeyguardTransitionInteractor + + @Before + fun setup() { + repository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.keyguardTransitionInteractor + } @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 25c157208513..0b34a01a0fe0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.authController import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -47,6 +48,7 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.enableSingleShade import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider @@ -123,6 +125,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer fun notificationsPlacement_dualShadeSmallClock_below() = kosmos.runTest { setupState( @@ -135,6 +138,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer fun notificationsPlacement_dualShadeLargeClock_topStart() = kosmos.runTest { setupState( @@ -156,6 +160,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer fun areNotificationsVisible_dualShadeWideOnLockscreen_true() = kosmos.runTest { setupState( @@ -298,6 +303,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa ) { val isShadeLayoutWide by collectLastValue(kosmos.shadeRepository.isShadeLayoutWide) val collectedClockSize by collectLastValue(kosmos.keyguardClockInteractor.clockSize) + val collectedShadeMode by collectLastValue(kosmos.shadeModeInteractor.shadeMode) when (shadeMode) { ShadeMode.Dual -> kosmos.enableDualShade(wideLayout = shadeLayoutWide) ShadeMode.Single -> kosmos.enableSingleShade() @@ -309,6 +315,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa if (shadeLayoutWide != null) { assertThat(isShadeLayoutWide).isEqualTo(shadeLayoutWide) } + assertThat(collectedShadeMode).isEqualTo(shadeMode) assertThat(collectedClockSize).isEqualTo(clockSize) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt index c775bfd75f6e..9e400a6c0a4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -34,6 +35,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class GridLayoutTypeInteractorTest : SysuiTestCase() { val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt index 2e7aeb433e04..9fe783b98046 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.data.repository.QSColumnsRepository @@ -76,6 +77,7 @@ class QSColumnsInteractorTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun withDualShade_returnsCorrectValue() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt index fdbf42c9afd8..d5e502e99de5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt @@ -21,6 +21,7 @@ import android.content.res.mainResources import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS @@ -36,6 +37,7 @@ import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -43,6 +45,7 @@ import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(ParameterizedAndroidJunit4::class) @SmallTest class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() { @@ -63,6 +66,7 @@ class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : Sysui } @Test + @EnableSceneContainer fun shouldMediaShowInRow() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt index 241cdbfbef83..4912c319bf2e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testCase @@ -88,6 +89,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationNull_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -111,6 +113,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -133,6 +136,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt index 668f568d7f46..d26e195d360a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos @@ -31,6 +32,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class ShadeModeInteractorImplTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -80,7 +82,7 @@ class ShadeModeInteractorImplTest : SysuiTestCase() { } @Test - fun isDualShade_settingEnabled_returnsTrue() = + fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() = testScope.runTest { // TODO(b/391578667): Add a test case for user switching once the bug is fixed. val shadeMode by collectLastValue(underTest.shadeMode) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt index b8f66acf6413..dde867814159 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -59,6 +60,7 @@ import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -103,6 +105,7 @@ class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableSceneContainer fun hydrateShadeMode_dualShadeEnabled() = testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index aaa9b58a45df..7cf817a06225 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -49,8 +49,11 @@ import com.android.systemui.statusbar.notification.shared.ActiveNotificationMode import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.runner.RunWith @@ -286,13 +289,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasShortCriticalText_usesTextInsteadOfTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.shortCriticalText = "Arrived" this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -340,13 +345,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_basicTime_timeHiddenIfAutomaticallyPromoted() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.wasPromotedAutomatically = true this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -370,13 +377,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_basicTime_timeShownIfNotAutomaticallyPromoted() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.wasPromotedAutomatically = false this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -397,18 +406,117 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_basicTime_isShortTimeDelta() = + fun chips_basicTime_timeInFuture_isShortTimeDelta() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 13.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeLessThanOneMinInFuture_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime + 500, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeIsNow_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 62.seconds.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeInPast_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 62.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime - 2.minutes.inWholeMilliseconds, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( listOf( activeNotificationModel( @@ -421,6 +529,45 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest).hasSize(1) assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + // Not necessarily the behavior we *want* to have, but it's the currently implemented behavior. + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeIsInFuture_thenTimeAdvances_stillShortTimeDelta() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime + 3.minutes.inWholeMilliseconds, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + + fakeSystemClock.advanceTime(5.minutes.inWholeMilliseconds) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) } @@ -429,12 +576,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_countUpTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.CountUp, ) } @@ -457,12 +606,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_countDownTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.CountDown, ) } @@ -485,12 +636,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_noHeadsUp_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -517,12 +670,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpBySystem_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -556,12 +711,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -569,7 +726,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { PromotedNotificationContentModel.Builder("other notif").apply { this.time = PromotedNotificationContentModel.When( - time = 654321L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -610,12 +767,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java index d66b010daefd..a58f7f72f08a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java @@ -51,8 +51,10 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.people.NotificationPersonExtractor; import com.android.systemui.util.DeviceConfigProxyFake; +import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java index 77fd06757595..8520508c7611 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java @@ -132,7 +132,7 @@ public class DynamicChildBindControllerTest extends SysuiTestCase { LayoutInflater inflater = LayoutInflater.from(mContext); inflater.setFactory2( new RowInflaterTask.RowAsyncLayoutInflater(entry, new FakeSystemClock(), mock( - RowInflaterTaskLogger.class))); + RowInflaterTaskLogger.class), mContext.getUser())); ExpandableNotificationRow row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row, null); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt new file mode 100644 index 000000000000..426af264da07 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 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.notification.collection + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class BundleEntryTest : SysuiTestCase() { + private lateinit var underTest: BundleEntry + + @get:Rule + val setFlagsRule = SetFlagsRule() + + @Before + fun setUp() { + underTest = BundleEntry("key") + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getParent_adapter() { + assertThat(underTest.entryAdapter.parent).isEqualTo(GroupEntry.ROOT_ENTRY) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isTopLevelEntry_adapter() { + assertThat(underTest.entryAdapter.isTopLevelEntry).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getRow_adapter() { + assertThat(underTest.entryAdapter.row).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_adapter() { + assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getKey_adapter() { + assertThat(underTest.entryAdapter.key).isEqualTo("key") + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java index 8e95ac599ce1..76e2d619a4df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java @@ -30,6 +30,9 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -39,8 +42,10 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -57,6 +62,9 @@ public class HighPriorityProviderTest extends SysuiTestCase { @Mock private GroupMembershipManager mGroupMembershipManager; private HighPriorityProvider mHighPriorityProvider; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -210,6 +218,7 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testIsHighPriority_checkChildrenToCalculatePriority_legacy() { // GIVEN: a summary with low priority has a highPriorityChild and a lowPriorityChild final NotificationEntry summary = createNotifEntry(false); @@ -247,20 +256,18 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test - public void testIsHighPriority_checkChildrenToCalculatePriority() { + public void testIsHighPriority_checkChildrenViewsToCalculatePriority() { // GIVEN: // parent with summary = lowPrioritySummary // NotificationEntry = lowPriorityChild // NotificationEntry = highPriorityChild + List<NotificationEntry> children = List.of(createNotifEntry(false), createNotifEntry(true)); final NotificationEntry lowPrioritySummary = createNotifEntry(false); final GroupEntry parentEntry = new GroupEntryBuilder() .setSummary(lowPrioritySummary) + .setChildren(children) .build(); - when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn( - new ArrayList<>( - List.of( - createNotifEntry(false), - createNotifEntry(true)))); + when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(children); // THEN the GroupEntry parentEntry is high priority since it has a high priority child assertTrue(mHighPriorityProvider.isHighPriority(parentEntry)); @@ -272,10 +279,11 @@ public class HighPriorityProviderTest extends SysuiTestCase { // parent with summary = lowPrioritySummary // NotificationEntry = lowPriorityChild final NotificationEntry lowPrioritySummary = createNotifEntry(false); + final NotificationEntry lowPriorityChild = createNotifEntry(false); final GroupEntry parentEntry = new GroupEntryBuilder() .setSummary(lowPrioritySummary) + .setChildren(List.of(lowPriorityChild)) .build(); - final NotificationEntry lowPriorityChild = createNotifEntry(false); when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn( new ArrayList<>(List.of(lowPriorityChild))); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt index e93c74252251..7fa157fa7cb3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt @@ -27,14 +27,29 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.buildNotificationEntry +import com.android.systemui.statusbar.notification.buildOngoingCallEntry +import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry import com.android.systemui.statusbar.notification.collection.buildEntry import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.notifPipeline +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.notification.promoted.domain.interactor.promotedNotificationsInteractor +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -59,7 +74,13 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { fun setup() { allowTestableLooperAsMainThread() - colorizedFgsCoordinator = ColorizedFgsCoordinator() + kosmos.statusBarNotificationChipsInteractor.start() + + colorizedFgsCoordinator = + ColorizedFgsCoordinator( + kosmos.applicationCoroutineScope, + kosmos.promotedNotificationsInteractor, + ) colorizedFgsCoordinator.attach(notifPipeline) sectioner = colorizedFgsCoordinator.sectioner } @@ -178,6 +199,37 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { verify(notifPipeline, never()).addPromoter(any()) } + @Test + @EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, + ) + fun comparatorPutsCallBeforeOther() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + kosmos.renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(kosmos.promotedNotificationsInteractor.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + + // VERIFY that the comparator puts the call before the ron + assertThat(sectioner.comparator!!.compare(callEntry, ronEntry)).isLessThan(0) + // VERIFY that the comparator puts the ron before the other + assertThat(sectioner.comparator!!.compare(ronEntry, otherEntry)).isLessThan(0) + } + private fun makeCallStyle(): Notification.CallStyle { val pendingIntent = PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index db5921d8bd36..3dd0982ba2ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -17,23 +17,29 @@ package com.android.systemui.statusbar.notification.collection.render import android.os.Build +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.assertLogsWtf +import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Assume import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never @@ -44,6 +50,9 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class GroupExpansionManagerTest : SysuiTestCase() { + @get:Rule + val setFlagsRule = SetFlagsRule() + private lateinit var underTest: GroupExpansionManagerImpl private val dumpManager: DumpManager = mock() @@ -52,8 +61,8 @@ class GroupExpansionManagerTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private lateinit var beforeRenderListListener: OnBeforeRenderListListener - private val summary1 = notificationEntry("foo", 1) - private val summary2 = notificationEntry("bar", 1) + private val summary1 = notificationSummaryEntry("foo", 1) + private val summary2 = notificationSummaryEntry("bar", 1) private val entries = listOf<ListEntry>( GroupEntryBuilder() @@ -82,15 +91,25 @@ class GroupExpansionManagerTest : SysuiTestCase() { private fun notificationEntry(pkg: String, id: Int) = NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() } + private fun notificationSummaryEntry(pkg: String, id: Int) = + NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build() + .apply { row = mock() } + @Before fun setUp() { whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2) + whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter)) + .thenReturn(summary1.entryAdapter) + whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter)) + .thenReturn(summary2.entryAdapter) + underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager) } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun notifyOnlyOnChange() { var listenerCalledCount = 0 underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } @@ -108,6 +127,25 @@ class GroupExpansionManagerTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun notifyOnlyOnChange_withEntryAdapter() { + var listenerCalledCount = 0 + underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } + + underTest.setGroupExpanded(summary1.entryAdapter, false) + assertThat(listenerCalledCount).isEqualTo(0) + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(1) + underTest.setGroupExpanded(summary2.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(2) + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(2) + underTest.setGroupExpanded(summary2.entryAdapter, false) + assertThat(listenerCalledCount).isEqualTo(3) + } + + @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun expandUnattachedEntry() { // First, expand the entry when it is attached. underTest.setGroupExpanded(summary1, true) @@ -122,6 +160,22 @@ class GroupExpansionManagerTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun expandUnattachedEntryAdapter() { + // First, expand the entry when it is attached. + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue() + + // Un-attach it, and un-expand it. + NotificationEntryBuilder.setNewParent(summary1, null) + underTest.setGroupExpanded(summary1.entryAdapter, false) + + // Expanding again should throw. + assertLogsWtf { underTest.setGroupExpanded(summary1.entryAdapter, true) } + } + + @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun syncWithPipeline() { underTest.attach(pipeline) beforeRenderListListener = withArgCaptor { @@ -143,4 +197,28 @@ class GroupExpansionManagerTest : SysuiTestCase() { verify(listener).onGroupExpansionChange(summary1.row, false) verifyNoMoreInteractions(listener) } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun syncWithPipeline_withEntryAdapter() { + underTest.attach(pipeline) + beforeRenderListListener = withArgCaptor { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + val listener: OnGroupExpansionChangeListener = mock() + underTest.registerGroupExpansionChangeListener(listener) + + beforeRenderListListener.onBeforeRenderList(entries) + verify(listener, never()).onGroupExpansionChange(any(), any()) + + // Expand one of the groups. + underTest.setGroupExpanded(summary1.entryAdapter, true) + verify(listener).onGroupExpansionChange(summary1.row, true) + + // Empty the pipeline list and verify that the group is no longer expanded. + beforeRenderListListener.onBeforeRenderList(emptyList()) + verify(listener).onGroupExpansionChange(summary1.row, false) + verifyNoMoreInteractions(listener) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt index 2cbcc5a8d925..dcbf44e6e301 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt @@ -16,34 +16,46 @@ package com.android.systemui.statusbar.notification.collection.render +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.google.common.truth.Truth.assertThat +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class GroupMembershipManagerTest : SysuiTestCase() { + + @get:Rule + val setFlagsRule = SetFlagsRule() + private var underTest = GroupMembershipManagerImpl() @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_topLevel() { val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_noParent() { val noParentEntry = NotificationEntryBuilder().setParent(null).build() assertThat(underTest.isChildInGroup(noParentEntry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_summary() { val groupKey = "group" val summary = @@ -57,12 +69,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_summary() { val groupKey = "group" val summary = @@ -76,6 +90,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_child() { val groupKey = "group" val summary = @@ -90,12 +105,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.getGroupSummary(entry)).isNull() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_summary() { val groupKey = "group" val summary = @@ -109,6 +126,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_child() { val groupKey = "group" val summary = @@ -121,4 +139,104 @@ class GroupMembershipManagerTest : SysuiTestCase() { assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary) } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_topLevel() { + val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.isChildInGroup(topLevelEntry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_noParent() { + val noParentEntry = NotificationEntryBuilder().setParent(null).build() + assertThat(underTest.isChildInGroup(noParentEntry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.isChildInGroup(summary.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_topLevelEntry() { + val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.isGroupRoot(summary.entryAdapter)).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_child() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() + + assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_topLevelEntry() { + val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_child() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() + + assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 339f8fac3820..e22acd53e584 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -106,11 +106,15 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME) this.addOverride( R.integer.heads_up_notification_minimum_time, - TEST_MINIMUM_DISPLAY_TIME, + TEST_MINIMUM_DISPLAY_TIME_DEFAULT, ) this.addOverride( R.integer.heads_up_notification_minimum_time_with_throttling, - TEST_MINIMUM_DISPLAY_TIME, + TEST_MINIMUM_DISPLAY_TIME_DEFAULT, + ) + this.addOverride( + R.integer.heads_up_notification_minimum_time_for_user_initiated, + TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED, ) this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME) this.addOverride( @@ -414,7 +418,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun testRemoveNotification_beforeMinimumDisplayTime() { + fun testRemoveNotification_beforeMinimumDisplayTime_notUserInitiatedHun() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) useAccessibilityTimeout(false) @@ -429,18 +433,22 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(removedImmediately).isFalse() assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() - systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()) + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() } @Test - fun testRemoveNotification_afterMinimumDisplayTime() { + fun testRemoveNotification_afterMinimumDisplayTime_notUserInitiatedHun() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) useAccessibilityTimeout(false) underTest.showNotification(entry) - systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()) + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() @@ -455,6 +463,57 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testRemoveNotification_beforeMinimumDisplayTime_forUserInitiatedHun() { + useAccessibilityTimeout(false) + + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + entry.row = testHelper.createRow() + underTest.showNotification(entry, isPinnedByUser = true) + + val removedImmediately = + underTest.removeNotification( + entry.key, + /* releaseImmediately = */ false, + "beforeMinimumDisplayTime", + ) + assertThat(removedImmediately).isFalse() + assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() + + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) + + assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testRemoveNotification_afterMinimumDisplayTime_forUserInitiatedHun() { + useAccessibilityTimeout(false) + + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + entry.row = testHelper.createRow() + underTest.showNotification(entry, isPinnedByUser = true) + + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) + + assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() + + val removedImmediately = + underTest.removeNotification( + entry.key, + /* releaseImmediately = */ false, + "afterMinimumDisplayTime", + ) + + assertThat(removedImmediately).isTrue() + assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() + } + + @Test fun testRemoveNotification_releaseImmediately() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) @@ -1047,16 +1106,21 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } companion object { - const val TEST_TOUCH_ACCEPTANCE_TIME = 200 - const val TEST_A11Y_AUTO_DISMISS_TIME = 1000 - const val TEST_EXTENSION_TIME = 500 + private const val TEST_TOUCH_ACCEPTANCE_TIME = 200 + private const val TEST_A11Y_AUTO_DISMISS_TIME = 1000 + private const val TEST_EXTENSION_TIME = 500 - const val TEST_MINIMUM_DISPLAY_TIME = 400 - const val TEST_AUTO_DISMISS_TIME = 600 - const val TEST_STICKY_AUTO_DISMISS_TIME = 800 + private const val TEST_MINIMUM_DISPLAY_TIME_DEFAULT = 400 + private const val TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED = 500 + private const val TEST_AUTO_DISMISS_TIME = 600 + private const val TEST_STICKY_AUTO_DISMISS_TIME = 800 init { - assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME) + assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT) + .isLessThan(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED) + assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT).isLessThan(TEST_AUTO_DISMISS_TIME) + assertThat(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED) + .isLessThan(TEST_AUTO_DISMISS_TIME) assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME) assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt new file mode 100644 index 000000000000..75f5de0118d4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2025 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.notification.people + +import android.app.Notification +import android.app.NotificationChannel +import android.content.pm.ShortcutInfo +import android.service.notification.NotificationListenerService.Ranking +import android.service.notification.StatusBarNotification +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_FULL_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mock + + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PeopleNotificationIdentifierTest : SysuiTestCase() { + + private lateinit var underTest: PeopleNotificationIdentifierImpl + + private val summary1 = notificationEntry("foo", 1, summary = true) + private val summary2 = notificationEntry("bar", 1, summary = true) + private val entries = + listOf<GroupEntry>( + GroupEntryBuilder() + .setSummary(summary1) + .setChildren( + listOf( + notificationEntry("foo", 2), + notificationEntry("foo", 3), + notificationEntry("foo", 4) + ) + ) + .build(), + GroupEntryBuilder() + .setSummary(summary2) + .setChildren( + listOf( + notificationEntry("bar", 2), + notificationEntry("bar", 3), + notificationEntry("bar", 4) + ) + ) + .build() + ) + + private fun notificationEntry( + pkg: String, + id: Int, + summary: Boolean = false + ): NotificationEntry { + val sbn = mock(StatusBarNotification::class.java) + Mockito.`when`(sbn.key).thenReturn("key") + Mockito.`when`(sbn.notification).thenReturn(mock(Notification::class.java)) + if (summary) + Mockito.`when`(sbn.notification.isGroupSummary).thenReturn(true) + return NotificationEntryBuilder().setPkg(pkg) + .setId(id) + .setSbn(sbn) + .build().apply { + row = mock(ExpandableNotificationRow::class.java) + } + } + + private fun personRanking(entry: NotificationEntry, personType: Int): Ranking { + val channel = NotificationChannel("person", "person", 4) + channel.setConversationId("parent", "person") + channel.setImportantConversation(true) + + val br = RankingBuilder(entry.ranking) + + when (personType) { + TYPE_NON_PERSON -> br.setIsConversation(false) + TYPE_PERSON -> { + br.setIsConversation(true) + br.setShortcutInfo(null) + } + + TYPE_IMPORTANT_PERSON -> { + br.setIsConversation(true) + br.setShortcutInfo(mock(ShortcutInfo::class.java)) + br.setChannel(channel) + } + + else -> { + br.setIsConversation(true) + br.setShortcutInfo(mock(ShortcutInfo::class.java)) + } + } + + return br.build() + } + + @Before + fun setUp() { + val personExtractor = object : NotificationPersonExtractor { + public override fun isPersonNotification(sbn: StatusBarNotification): Boolean { + return true + } + } + + underTest = PeopleNotificationIdentifierImpl( + personExtractor, + GroupMembershipManagerImpl() + ) + } + + private val Ranking.personTypeInfo + get() = when { + !isConversation -> TYPE_NON_PERSON + conversationShortcutInfo == null -> TYPE_PERSON + channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON + else -> TYPE_FULL_PERSON + } + + @Test + fun getPeopleNotificationType_entryIsImportant() { + summary1.setRanking(personRanking(summary1, TYPE_IMPORTANT_PERSON)) + + assertThat(underTest.getPeopleNotificationType(summary1)).isEqualTo(TYPE_IMPORTANT_PERSON) + } + + @Test + fun getPeopleNotificationType_importantChild() { + entries.get(0).getChildren().get(0).setRanking( + personRanking(entries.get(0).getChildren().get(0), TYPE_IMPORTANT_PERSON) + ) + + assertThat(entries.get(0).summary?.let { underTest.getPeopleNotificationType(it) }) + .isEqualTo(TYPE_IMPORTANT_PERSON) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt new file mode 100644 index 000000000000..aa6e76d08c17 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt @@ -0,0 +1,156 @@ +/* + * 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.notification.promoted.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.buildNotificationEntry +import com.android.systemui.statusbar.notification.buildOngoingCallEntry +import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, +) +class PromotedNotificationsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Fixture { promotedNotificationsInteractor } + + @Before + fun setUp() { + kosmos.statusBarNotificationChipsInteractor.start() + } + + @Test + fun orderedChipNotificationKeys_containsNonPromotedCalls() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + } + + @Test + fun orderedChipNotificationKeys_containsPromotedCalls() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = true) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + } + + @Test + fun topPromotedNotificationContent_skipsNonPromotedCalls() = + kosmos.runTest { + // GIVEN a non-promoted call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN the ron is first because the call has no content + assertThat(topPromotedNotificationContent?.identity?.key) + .isEqualTo("0|test_pkg|0|ron|0") + } + + @Test + fun topPromotedNotificationContent_includesPromotedCalls() = + kosmos.runTest { + // GIVEN a promoted call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = true) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN the call is the top notification + assertThat(topPromotedNotificationContent?.identity?.key) + .isEqualTo("0|test_pkg|0|call|0") + } + + @Test + fun topPromotedNotificationContent_nullWithNoPromotedNotifications() = + kosmos.runTest { + // GIVEN a a non-promoted call and no promoted ongoing entry + val callEntry = buildOngoingCallEntry(promoted = false) + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList(listOf(callEntry, otherEntry)) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN there is no top promoted notification + assertThat(topPromotedNotificationContent).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 7d406b4b397c..9f35d631bd45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -67,6 +67,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; @@ -248,14 +249,13 @@ public class NotificationContentInflaterTest extends SysuiTestCase { true /* isNewView */, (v, p, r) -> true, new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, - Exception e) { + public void handleInflationException(Exception e) { countDownLatch.countDown(); throw new RuntimeException("No Exception expected"); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { countDownLatch.countDown(); } }, mRow.getPrivateLayout(), null, null, new HashMap<>(), @@ -539,8 +539,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { inflater.setInflateSynchronously(true); InflationCallback callback = new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, - Exception e) { + public void handleInflationException(Exception e) { if (!expectingException) { exceptionHolder.setException(e); } @@ -548,7 +547,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { if (expectingException) { exceptionHolder.setException(new RuntimeException( "Inflation finished even though there should be an error")); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 82eca3735a71..ce3aee1d88d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTIO import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi @@ -223,12 +224,12 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { remoteViewClickHandler = { _, _, _ -> true }, callback = object : InflationCallback { - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { countDownLatch.countDown() throw RuntimeException("No Exception expected") } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { countDownLatch.countDown() } }, @@ -675,14 +676,14 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { inflater.setInflateSynchronously(true) val callback: InflationCallback = object : InflationCallback { - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { if (!expectingException) { exceptionHolder.exception = e } countDownLatch.countDown() } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { if (expectingException) { exceptionHolder.exception = RuntimeException( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index c39b252cd795..f2131da8f0bb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -615,7 +615,7 @@ public class NotificationTestHelper { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock, - mRowInflaterTaskLogger)); + mRowInflaterTaskLogger, UserHandle.of(entry.getSbn().getNormalizedUserId()))); mRow = (ExpandableNotificationRow) inflater.inflate( R.layout.status_bar_notification_row, null /* root */, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index d570f18e35d8..6381b4e0fef7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -57,11 +57,12 @@ class NotificationShelfViewModelTest : SysuiTestCase() { statusBarStateController = mock() whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true) } - private val underTest = kosmos.notificationShelfViewModel private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val keyguardRepository = kosmos.fakeKeyguardRepository - private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController private val powerRepository = kosmos.fakePowerRepository + private val keyguardTransitionController by lazy { kosmos.lockscreenShadeTransitionController } + + private val underTest by lazy { kosmos.notificationShelfViewModel } @Test fun canModifyColorOfNotifications_whenKeyguardNotShowing() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 256da253588c..9c5d65ec12ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.statusbar.notification.stack +import android.os.UserHandle import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper @@ -21,6 +22,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState @@ -978,7 +980,10 @@ open class NotificationShelfTest : SysuiTestCase() { ) { val sbnMock: StatusBarNotification = mock() val mockEntry = mock<NotificationEntry>().apply { whenever(this.sbn).thenReturn(sbnMock) } - val row = ExpandableNotificationRow(mContext, null, mockEntry) + val row = when (NotificationBundleUi.isEnabled) { + true -> ExpandableNotificationRow(mContext, null, UserHandle.CURRENT) + false -> ExpandableNotificationRow(mContext, null, mockEntry) + } whenever(ambientState.lastVisibleBackgroundChild).thenReturn(row) whenever(ambientState.isExpansionChanging).thenReturn(true) whenever(ambientState.expansionFraction).thenReturn(expansionFraction) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java index 8ec17dadcfe7..345ddae42798 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java @@ -46,9 +46,11 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntry.NotifEntryAdapter; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationContentView; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; @@ -133,9 +135,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); + final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); + when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter); when(enr.isChildInGroup()).thenReturn(true); when(enr.areChildrenExpanded()).thenReturn(false); @@ -144,7 +148,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + if (NotificationBundleUi.isEnabled()) { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter); + } else { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + } verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -169,7 +177,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -193,7 +202,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -207,9 +217,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); + final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); + when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter); when(enr.isChildInGroup()).thenReturn(true); when(enr.areChildrenExpanded()).thenReturn(false); @@ -218,7 +230,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + if (NotificationBundleUi.isEnabled()) { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter); + } else { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + } verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -244,6 +260,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { // THEN verify(mGroupExpansionManager, never()).toggleGroupExpansion(enrEntry); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -272,7 +289,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr).toggleExpansionState(); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -299,7 +317,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr, never()).toggleExpansionState(); verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -326,7 +345,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr).toggleExpansionState(); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -353,6 +373,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr, never()).toggleExpansionState(); verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java new file mode 100644 index 000000000000..ecd04a47b8ae --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 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.theme; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + + +public class HardwareColorRule implements TestRule { + public String color = ""; + public String[] options = {}; + public boolean isTesting = false; + + @Override + public Statement apply(Statement base, Description description) { + HardwareColors hardwareColors = description.getAnnotation(HardwareColors.class); + if (hardwareColors != null) { + color = hardwareColors.color(); + options = hardwareColors.options(); + isTesting = true; + } + return base; + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java new file mode 100644 index 000000000000..0b8df2e2670e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 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.theme; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface HardwareColors { + String color(); + String[] options(); +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 5cd0846ded7e..9a0b8125fb25 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -64,6 +64,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.monet.DynamicColors; @@ -77,6 +78,7 @@ import com.android.systemui.util.settings.SecureSettings; import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -98,6 +100,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { private static final UserHandle MANAGED_USER_HANDLE = UserHandle.of(100); private static final UserHandle PRIVATE_USER_HANDLE = UserHandle.of(101); + @Rule + public HardwareColorRule rule = new HardwareColorRule(); + @Mock private JavaAdapter mJavaAdapter; @Mock @@ -148,13 +153,17 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<ContentObserver> mSettingsObserver; + @Mock + private SystemPropertiesHelper mSystemProperties; + @Before public void setup() { MockitoAnnotations.initMocks(this); + when(mFeatureFlags.isEnabled(Flags.MONET)).thenReturn(true); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); when(mUiModeManager.getContrast()).thenReturn(0.5f); - when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); + when(mResources.getColor(eq(android.R.color.system_accent1_500), any())) .thenReturn(Color.RED); when(mResources.getColor(eq(android.R.color.system_accent2_500), any())) @@ -166,11 +175,20 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { when(mResources.getColor(eq(android.R.color.system_neutral2_500), any())) .thenReturn(Color.BLACK); + when(mResources.getStringArray(com.android.internal.R.array.theming_defaults)) + .thenReturn(rule.options); + + // should fallback to `*|TONAL_SPOT|home_wallpaper` + when(mSystemProperties.get("ro.boot.hardware.color")).thenReturn(rule.color); + // will try set hardware colors as boot ONLY if user is not set yet + when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(!rule.isTesting); + mThemeOverlayController = new ThemeOverlayController(mContext, mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -214,11 +232,58 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { public void start_checksWallpaper() { ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); verify(mBgExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + } + + @Test + @HardwareColors(color = "BLK", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_checkHardwareColor() { + // getWallpaperColors should not be called + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager, never()).getWallpaperColors(anyInt()); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.MONOCHROMATIC); + assertThat(mThemeOverlayController.mCurrentColors.get(0).getMainColors().get( + 0).toArgb()).isEqualTo(Color.RED); + } + + @Test + @HardwareColors(color = "", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_wildcardColor() { + // getWallpaperColors will be called because we srt wildcard to `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.VIBRANT); + } + + @Test + @HardwareColors(color = "NONEXISTENT", options = {}) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_fallbackColor() { + // getWallpaperColors will be called because we default color source is `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); registrationRunnable.getValue().run(); verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.TONAL_SPOT); } + @Test public void onWallpaperColorsChanged_setsTheme_whenForeground() { // Should ask for a new theme when wallpaper colors change @@ -287,9 +352,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"preset\"}"; + String jsonString = createJsonString(TestColorSource.preset, "override.package.name", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -313,11 +378,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -348,11 +409,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -381,11 +438,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -404,11 +457,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -455,8 +504,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Test public void onSettingChanged_invalidStyle() { when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true); - String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.theme_style\":\"some_invalid_name\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "A16B00", + "some_invalid_name"); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -473,11 +522,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -506,11 +551,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -537,11 +578,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -570,11 +607,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -599,7 +632,6 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } - @Test @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI) public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() { @@ -608,11 +640,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -642,11 +670,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -676,11 +700,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -711,11 +731,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -745,11 +761,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -886,7 +898,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -926,7 +939,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -992,7 +1006,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1018,7 +1032,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1034,8 +1048,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"00FF00\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "00FF00", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -1115,4 +1130,25 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { + DynamicColors.getCustomColorsMapped(false).size() * 2) ).setResourceValue(any(String.class), eq(TYPE_INT_COLOR_ARGB8), anyInt(), eq(null)); } + + private enum TestColorSource { + preset, + home_wallpaper, + lock_wallpaper + } + + private String createJsonString(TestColorSource colorSource, String seedColorHex, + String style) { + return "{\"android.theme.customization.color_source\":\"" + colorSource.toString() + "\"," + + "\"android.theme.customization.system_palette\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.accent_color\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.color_index\":\"2\"," + + "\"android.theme.customization.theme_style\":\"" + style + "\"}"; + } + + private String createJsonString(TestColorSource colorSource) { + return createJsonString(colorSource, "A16B00", "TONAL_SPOT"); + } + + } diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml index 61d6a9046144..a27e29f1beb6 100644 --- a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml @@ -19,12 +19,13 @@ android:height="40dp" android:viewportHeight="40" android:viewportWidth="40"> + <group> + <clip-path android:pathData="M8,12h24.5v15.5h-24.5z" /> <path - android:fillColor="#F7DAEE" - android:fillType="evenOdd" - android:pathData="M20.76,19C21.65,19 22.096,17.924 21.467,17.294L19.284,15.105C18.895,14.716 18.895,14.085 19.285,13.695C19.674,13.306 20.306,13.306 20.695,13.695L26.293,19.293C26.683,19.683 26.683,20.317 26.293,20.707L20.705,26.295C20.315,26.685 19.683,26.686 19.292,26.298C18.9,25.907 18.898,25.272 19.29,24.88L21.463,22.707C22.093,22.077 21.647,21 20.756,21H10C9.448,21 9,20.552 9,20C9,19.448 9.448,19 10,19H20.76ZM32,26C32,26.552 31.552,27 31,27C30.448,27 30,26.552 30,26V14C30,13.448 30.448,13 31,13C31.552,13 32,13.448 32,14V26Z" - android:strokeColor="#F7DAEE" - android:strokeLineCap="round" - android:strokeLineJoin="round" - android:strokeWidth="2" /> + android:fillColor="#000000" + android:pathData="M30.75,12C29.79,12 29,12.79 29,13.75V25.75C29,26.71 29.79,27.5 30.75,27.5C31.71,27.5 32.5,26.71 32.5,25.75V13.75C32.5,12.79 31.71,12 30.75,12Z" /> + <path + android:fillColor="#000000" + android:pathData="M20.98,12.92C20.3,12.24 19.19,12.24 18.51,12.92C17.83,13.6 17.83,14.71 18.51,15.39L21.12,18H9.75C8.79,18 8,18.79 8,19.75C8,20.71 8.79,21.5 9.75,21.5H21.11L18.51,24.1C18.18,24.43 18,24.87 18,25.34C18,25.81 18.18,26.25 18.52,26.58C18.86,26.92 19.31,27.09 19.75,27.09C20.19,27.09 20.65,26.92 20.99,26.58L26.61,20.96C27.28,20.29 27.28,19.21 26.61,18.55L20.98,12.92Z" /> + </group> </vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml deleted file mode 100644 index 044656d6fc7d..000000000000 --- a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml +++ /dev/null @@ -1,25 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="40dp" - android:height="40dp" - android:viewportHeight="40" - android:viewportWidth="40"> - <path - android:fillColor="#ECDFE5" - android:pathData="M18.792,26.5L23.333,21.958L27.875,26.5L29.875,24.542L25.292,20L29.792,15.458L27.833,13.5L23.333,18.042L18.792,13.5L16.792,15.458L21.375,20L16.792,24.542L18.792,26.5ZM14.708,33.333C14.292,33.333 13.875,33.236 13.458,33.042C13.069,32.847 12.75,32.569 12.5,32.208L3.333,20L12.458,7.792C12.708,7.431 13.028,7.153 13.417,6.958C13.833,6.764 14.264,6.667 14.708,6.667H33.917C34.694,6.667 35.347,6.944 35.875,7.5C36.431,8.028 36.708,8.681 36.708,9.458V30.542C36.708,31.319 36.431,31.986 35.875,32.542C35.347,33.069 34.694,33.333 33.917,33.333H14.708Z" /> -</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml new file mode 100644 index 000000000000..86f95bc97169 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <path + android:fillColor="#000000" + android:pathData="M22.167,21.9L25.531,25.265C25.795,25.502 26.112,25.621 26.481,25.621C26.851,25.621 27.167,25.502 27.431,25.265C27.669,25.001 27.788,24.684 27.788,24.315C27.788,23.919 27.669,23.589 27.431,23.325L24.067,20L27.392,16.675C27.656,16.411 27.788,16.094 27.788,15.725C27.788,15.356 27.656,15.039 27.392,14.775C27.128,14.511 26.798,14.379 26.402,14.379C26.033,14.379 25.729,14.511 25.492,14.775L22.167,18.1L18.802,14.735C18.538,14.498 18.222,14.379 17.852,14.379C17.483,14.379 17.166,14.498 16.902,14.735C16.665,14.999 16.546,15.329 16.546,15.725C16.546,16.094 16.665,16.411 16.902,16.675L20.267,20L16.902,23.325C16.665,23.589 16.546,23.906 16.546,24.275C16.546,24.644 16.665,24.961 16.902,25.225C17.166,25.489 17.483,25.621 17.852,25.621C18.248,25.621 18.578,25.489 18.842,25.225L22.167,21.9ZM14.012,32.667C13.59,32.667 13.181,32.574 12.785,32.39C12.416,32.179 12.099,31.915 11.835,31.598L4.394,21.623C4.024,21.148 3.84,20.607 3.84,20C3.84,19.393 4.024,18.852 4.394,18.377L11.835,8.402C12.073,8.085 12.39,7.835 12.785,7.65C13.181,7.439 13.59,7.333 14.012,7.333H32.142C32.907,7.333 33.554,7.597 34.081,8.125C34.609,8.653 34.873,9.286 34.873,10.025V29.975C34.873,30.714 34.609,31.347 34.081,31.875C33.554,32.403 32.907,32.667 32.142,32.667H14.012Z" /> +</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml new file mode 100644 index 000000000000..7f551f4d3c60 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml @@ -0,0 +1,31 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <group> + <clip-path android:pathData="M5,7h29.89v25h-29.89z" /> + <path + android:fillColor="#000000" + android:pathData="M30.96,32H15.59C14.21,32 12.89,31.34 12.06,30.24L5.78,21.86C4.74,20.47 4.74,18.54 5.78,17.15L12.06,8.77C12.89,7.67 14.21,7 15.59,7H30.96C33.13,7 34.89,8.76 34.89,10.93V28.08C34.89,30.25 33.13,32.01 30.96,32.01V32ZM14.46,28.44C14.73,28.79 15.15,29 15.59,29H30.96C31.47,29 31.89,28.58 31.89,28.07V10.93C31.89,10.42 31.47,10 30.96,10H15.59C15.15,10 14.73,10.21 14.46,10.56L8.18,18.94C7.93,19.27 7.93,19.72 8.18,20.05L14.46,28.43V28.44Z" /> + <path + android:fillColor="#000000" + android:pathData="M22.46,21.27L25.36,24.17C25.6,24.43 25.89,24.56 26.25,24.56C26.61,24.56 26.9,24.43 27.14,24.17C27.4,23.93 27.53,23.64 27.53,23.28C27.53,22.92 27.4,22.63 27.14,22.39L24.24,19.49L27.14,16.59C27.38,16.35 27.49,16.06 27.49,15.7C27.49,15.34 27.37,15.05 27.14,14.81C26.91,14.57 26.61,14.46 26.25,14.46C25.89,14.46 25.59,14.58 25.33,14.81L22.46,17.71L19.56,14.81C19.32,14.55 19.03,14.42 18.67,14.42C18.31,14.42 18.02,14.55 17.78,14.81C17.52,15.05 17.39,15.34 17.39,15.7C17.39,16.06 17.52,16.35 17.78,16.59L20.68,19.49L17.78,22.39C17.52,22.63 17.39,22.92 17.39,23.28C17.39,23.64 17.52,23.93 17.78,24.17C18.02,24.41 18.31,24.52 18.67,24.52C19.03,24.52 19.32,24.4 19.56,24.17L22.46,21.27Z" /> + </group> +</vector> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 6a4a0b7893d9..8d10e393b5ca 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -175,6 +175,9 @@ <!-- Minimum display time for a heads up notification if throttling is enabled, in milliseconds. --> <integer name="heads_up_notification_minimum_time_with_throttling">500</integer> + <!-- Minimum display time for a heads up notification that was shown from a user action (like tapping on a different part of the UI), in milliseconds. --> + <integer name="heads_up_notification_minimum_time_for_user_initiated">3000</integer> + <!-- Display time for a sticky heads up notification, in milliseconds. --> <integer name="sticky_heads_up_notification_time">60000</integer> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index 245283da75ab..04d4c2a3cdf9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -184,7 +184,9 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView } mDeleteButton = findViewById(R.id.delete_button); if (Flags.bouncerUiRevamp2()) { - mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete); + mDeleteButton.setDrawableForTransparentMode(R.drawable.pin_bouncer_delete_filled); + mDeleteButton.setDefaultDrawable(R.drawable.pin_bouncer_delete_outline); + mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete_outline); } mDeleteButton.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 0ff93236a856..584ebb50520a 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -25,6 +25,7 @@ import android.util.AttributeSet; import android.view.MotionEvent; import android.view.accessibility.AccessibilityNodeInfo; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import com.android.systemui.Flags; @@ -42,6 +43,12 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni private int mStyleAttr; private boolean mIsTransparentMode; + @DrawableRes + private int mDrawableForTransparentMode = 0; + + @DrawableRes + private int mDefaultDrawable = 0; + public NumPadButton(Context context, AttributeSet attrs) { super(context, attrs); mStyleAttr = attrs.getStyleAttribute(); @@ -123,8 +130,14 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni mIsTransparentMode = isTransparentMode; if (isTransparentMode) { + if (mDrawableForTransparentMode != 0) { + setImageResource(mDrawableForTransparentMode); + } setBackgroundColor(getResources().getColor(android.R.color.transparent)); } else { + if (mDefaultDrawable != 0) { + setImageResource(mDefaultDrawable); + } Drawable bgDrawable = getContext().getDrawable(R.drawable.num_pad_key_background); if (Flags.bouncerUiRevamp2() && bgDrawable != null) { bgDrawable.setTint(Color.actionBg); @@ -154,4 +167,19 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni super.onInitializeAccessibilityNodeInfo(info); info.setTextEntryKey(true); } + + /** + * Drawable to use when transparent mode is enabled + */ + public void setDrawableForTransparentMode(@DrawableRes int drawableResId) { + mDrawableForTransparentMode = drawableResId; + } + + /** + * Drawable to use when transparent mode is not enabled. + */ + public void setDefaultDrawable(@DrawableRes int drawableResId) { + mDefaultDrawable = drawableResId; + setImageResource(mDefaultDrawable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt index 8f4e8701cad8..1ab0b93da175 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.scene.domain.SceneFrameworkTableLog +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository @@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn /** @@ -89,10 +91,14 @@ constructor( ) : ShadeModeInteractor { private val isDualShadeEnabled: Flow<Boolean> = - secureSettingsRepository.boolSetting( - Settings.Secure.DUAL_SHADE, - defaultValue = DUAL_SHADE_ENABLED_DEFAULT, - ) + if (SceneContainerFlag.isEnabled) { + secureSettingsRepository.boolSetting( + Settings.Secure.DUAL_SHADE, + defaultValue = DUAL_SHADE_ENABLED_DEFAULT, + ) + } else { + flowOf(false) + } override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt index d20a2d18a7e7..edb44185459c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -145,7 +145,7 @@ constructor( * Emits all notifications that are eligible to show as chips in the status bar. This is * different from which chips will *actually* show, see [shownNotificationChips] for that. */ - private val allNotificationChips: Flow<List<NotificationChipModel>> = + val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { // For all our current interactors... // TODO(b/364653005): When a promoted notification is added or removed, each individual diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 3ecbdf82f2cb..11e9fd56288f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -51,6 +52,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, + private val systemClock: SystemClock, ) { /** * A flow modeling the notification chips that should be shown. Emits an empty list if there are @@ -158,16 +160,38 @@ constructor( clickBehavior, ) } + when (this.promotedContent.time.mode) { PromotedNotificationContentModel.When.Mode.BasicTime -> { - return OngoingActivityChipModel.Active.ShortTimeDelta( - this.key, - icon, - colors, - time = this.promotedContent.time.time, - onClickListenerLegacy, - clickBehavior, - ) + return if ( + this.promotedContent.time.time >= + systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS + ) { + OngoingActivityChipModel.Active.ShortTimeDelta( + this.key, + icon, + colors, + time = this.promotedContent.time.time, + onClickListenerLegacy, + clickBehavior, + ) + } else { + // Don't show a `when` time that's close to now or in the past because it's + // likely that the app didn't intentionally set the `when` time to be shown in + // the status bar chip. + // TODO(b/393369213): If a notification sets a `when` time in the future and + // then that time comes and goes, the chip *will* start showing times in the + // past. Not going to fix this right now because the Compose implementation + // automatically handles this for us and we're hoping to launch the notification + // chips at the same time as the Compose chips. + return OngoingActivityChipModel.Active.IconOnly( + this.key, + icon, + colors, + onClickListenerLegacy, + clickBehavior, + ) + } } PromotedNotificationContentModel.When.Mode.CountUp -> { return OngoingActivityChipModel.Active.Timer( @@ -204,4 +228,12 @@ constructor( ) ) } + + companion object { + /** + * Notifications must have a `when` time of at least 1 minute in the future in order for the + * status bar chip to show the time. + */ + private const val FUTURE_TIME_THRESHOLD_MILLIS = 60 * 1000 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 37485feed792..0e3f103c152e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -16,11 +16,74 @@ package com.android.systemui.statusbar.notification.collection; +import static android.app.NotificationChannel.NEWS_ID; +import static android.app.NotificationChannel.PROMOTIONS_ID; +import static android.app.NotificationChannel.RECS_ID; +import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + +import java.util.List; + /** * Abstract class to represent notification section bundled by AI. */ public class BundleEntry extends PipelineEntry { + private final String mKey; + private final BundleEntryAdapter mEntryAdapter; + + // TODO (b/389839319): implement the row + private ExpandableNotificationRow mRow; + + public BundleEntry(String key) { + mKey = key; + mEntryAdapter = new BundleEntryAdapter(); + } + + @VisibleForTesting + public BundleEntryAdapter getEntryAdapter() { + return mEntryAdapter; + } + public class BundleEntryAdapter implements EntryAdapter { + + /** + * TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated? + */ + @Override + public GroupEntry getParent() { + return GroupEntry.ROOT_ENTRY; + } + + @Override + public boolean isTopLevelEntry() { + return true; + } + + @Override + public String getKey() { + return mKey; + } + + @Override + public ExpandableNotificationRow getRow() { + return mRow; + } + + @Nullable + @Override + public EntryAdapter getGroupRoot() { + return this; + } } + + public static final List<BundleEntry> ROOT_BUNDLES = List.of( + new BundleEntry(PROMOTIONS_ID), + new BundleEntry(SOCIAL_MEDIA_ID), + new BundleEntry(NEWS_ID), + new BundleEntry(RECS_ID)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index b12b1c538a32..4df81c97e21e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -16,8 +16,52 @@ package com.android.systemui.statusbar.notification.collection; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + /** * Adapter interface for UI to get relevant info. */ public interface EntryAdapter { + + /** + * Gets the parent of this entry, or null if the entry's view is not attached + */ + @Nullable PipelineEntry getParent(); + + /** + * Returns whether the entry is attached and appears at the top level of the shade + */ + boolean isTopLevelEntry(); + + /** + * @return the unique identifier for this entry + */ + @NonNull String getKey(); + + /** + * Gets the view that this entry is backing. + */ + @NonNull + ExpandableNotificationRow getRow(); + + /** + * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry + * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the + * BundleEntry will be returned. If the given notification is a group summary NotificationEntry, + * or a child of a group summary, the summary NotificationEntry will be returned, even if that + * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any + * group or bundle grouping, null will be returned. + */ + @Nullable + EntryAdapter getGroupRoot(); + + /** + * Returns whether the entry is attached to the current shade list + */ + default boolean isAttached() { + return getParent() != null; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java index fc47dc1ed81a..8f3c357a277a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import javax.inject.Inject; @@ -78,7 +79,7 @@ public class NotifInflaterImpl implements NotifInflater { requireBinder().inflateViews( entry, params, - wrapInflationCallback(callback)); + wrapInflationCallback(entry, callback)); } catch (InflationException e) { mLogger.logInflationException(entry, e); mNotifErrorManager.setInflationError(entry, e); @@ -101,17 +102,26 @@ public class NotifInflaterImpl implements NotifInflater { } private NotificationRowContentBinder.InflationCallback wrapInflationCallback( + final NotificationEntry entry, InflationCallback callback) { return new NotificationRowContentBinder.InflationCallback() { @Override public void handleInflationException( NotificationEntry entry, Exception e) { + if (NotificationBundleUi.isEnabled()) { + handleInflationException(e); + } else { + mNotifErrorManager.setInflationError(entry, e); + } + } + @Override + public void handleInflationException(Exception e) { mNotifErrorManager.setInflationError(entry, e); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { mNotifErrorManager.clearInflationError(entry); if (callback != null) { callback.onInflationFinished(entry, entry.getRowController()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 7dd82a6b5198..90f9525c7683 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -29,6 +29,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES; +import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING; @@ -107,6 +109,7 @@ public final class NotificationEntry extends ListEntry { private final String mKey; private StatusBarNotification mSbn; private Ranking mRanking; + private final NotifEntryAdapter mEntryAdapter; /* * Bookkeeping members @@ -268,9 +271,48 @@ public final class NotificationEntry extends ListEntry { mKey = sbn.getKey(); setSbn(sbn); setRanking(ranking); + mEntryAdapter = new NotifEntryAdapter(); } public class NotifEntryAdapter implements EntryAdapter { + @Override + public PipelineEntry getParent() { + return NotificationEntry.this.getParent(); + } + + @Override + public boolean isTopLevelEntry() { + return getParent() != null + && (getParent() == ROOT_ENTRY || ROOT_BUNDLES.contains(getParent())); + } + + @Override + public String getKey() { + return NotificationEntry.this.getKey(); + } + + @Override + public ExpandableNotificationRow getRow() { + return NotificationEntry.this.getRow(); + } + + @Nullable + @Override + public EntryAdapter getGroupRoot() { + // TODO (b/395857098): for backwards compatibility this will return null if called + // on a group summary that's not in a bundles, but it should return itself. + if (isTopLevelEntry() || getParent() == null) { + return null; + } + if (NotificationEntry.this.getParent().getSummary() != null) { + return NotificationEntry.this.getParent().getSummary().mEntryAdapter; + } + return null; + } + } + + public EntryAdapter getEntryAdapter() { + return mEntryAdapter; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java index efedfef5cbe9..c5a479180329 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java @@ -19,5 +19,5 @@ package com.android.systemui.statusbar.notification.collection; /** * Class to represent a notification, group, or bundle in the pipeline. */ -public class PipelineEntry { +public abstract class PipelineEntry { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java index 733b986b5422..9df4bf4af4e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java @@ -23,6 +23,7 @@ import android.app.Notification; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -31,29 +32,50 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.promoted.domain.interactor.PromotedNotificationsInteractor; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; +import com.android.systemui.util.kotlin.JavaAdapterKt; -import com.google.common.primitives.Booleans; +import kotlinx.coroutines.CoroutineScope; + +import java.util.Collections; +import java.util.List; import javax.inject.Inject; /** * Handles sectioning for foreground service notifications. - * Puts non-min colorized foreground service notifications into the FGS section. See - * {@link NotifCoordinators} for section ordering priority. + * Puts non-min colorized foreground service notifications into the FGS section. See + * {@link NotifCoordinators} for section ordering priority. */ @CoordinatorScope public class ColorizedFgsCoordinator implements Coordinator { private static final String TAG = "ColorizedCoordinator"; + private final PromotedNotificationsInteractor mPromotedNotificationsInteractor; + private final CoroutineScope mMainScope; + + private List<String> mOrderedPromotedNotifKeys = Collections.emptyList(); @Inject - public ColorizedFgsCoordinator() { + public ColorizedFgsCoordinator( + @Application CoroutineScope mainScope, + PromotedNotificationsInteractor promotedNotificationsInteractor + ) { + mPromotedNotificationsInteractor = promotedNotificationsInteractor; + mMainScope = mainScope; } @Override - public void attach(NotifPipeline pipeline) { + public void attach(@NonNull NotifPipeline pipeline) { if (PromotedNotificationUi.isEnabled()) { pipeline.addPromoter(mPromotedOngoingPromoter); + + JavaAdapterKt.collectFlow(mMainScope, + mPromotedNotificationsInteractor.getOrderedChipNotificationKeys(), + (List<String> keys) -> { + mOrderedPromotedNotifKeys = keys; + mNotifSectioner.invalidateList("updated mOrderedPromotedNotifKeys"); + }); } } @@ -82,12 +104,24 @@ public class ColorizedFgsCoordinator implements Coordinator { return false; } - private NotifComparator mPreferPromoted = new NotifComparator("PreferPromoted") { + /** get the sort key for any entry in the ongoing section */ + private int getSortKey(@Nullable NotificationEntry entry) { + if (entry == null) return Integer.MAX_VALUE; + // Order all promoted notif keys first, using their order in the list + final int index = mOrderedPromotedNotifKeys.indexOf(entry.getKey()); + if (index >= 0) return index; + // Next, prioritize promoted ongoing over other notifications + return isPromotedOngoing(entry) ? Integer.MAX_VALUE - 1 : Integer.MAX_VALUE; + } + + private final NotifComparator mOngoingComparator = new NotifComparator( + "OngoingComparator") { @Override public int compare(@NonNull ListEntry o1, @NonNull ListEntry o2) { - return -1 * Booleans.compare( - isPromotedOngoing(o1.getRepresentativeEntry()), - isPromotedOngoing(o2.getRepresentativeEntry())); + return Integer.compare( + getSortKey(o1.getRepresentativeEntry()), + getSortKey(o2.getRepresentativeEntry()) + ); } }; @@ -95,7 +129,7 @@ public class ColorizedFgsCoordinator implements Coordinator { @Override public NotifComparator getComparator() { if (PromotedNotificationUi.isEnabled()) { - return mPreferPromoted; + return mOngoingComparator; } else { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java index d47fe20911f9..2e3ab926ad57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.List; @@ -129,21 +130,42 @@ public class HighPriorityProvider { >= NotificationManager.IMPORTANCE_DEFAULT); } + /** + * Returns whether the given ListEntry has a high priority child or is in a group with a child + * that's high priority + */ private boolean hasHighPriorityChild(ListEntry entry, boolean allowImplicit) { - if (entry instanceof NotificationEntry - && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) { - return false; - } - - List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry); - if (children != null) { - for (NotificationEntry child : children) { - if (child != entry && isHighPriority(child, allowImplicit)) { - return true; + if (NotificationBundleUi.isEnabled()) { + GroupEntry representativeGroupEntry = null; + if (entry instanceof GroupEntry) { + representativeGroupEntry = (GroupEntry) entry; + } else if (entry instanceof NotificationEntry){ + final NotificationEntry notificationEntry = entry.getRepresentativeEntry(); + if (notificationEntry.getParent() != null + && notificationEntry.getParent().getSummary() != null + && notificationEntry.getParent().getSummary() == notificationEntry) { + representativeGroupEntry = notificationEntry.getParent(); } } + return representativeGroupEntry != null && + representativeGroupEntry.getChildren().stream().anyMatch( + childEntry -> isHighPriority(childEntry, allowImplicit)); + + } else { + if (entry instanceof NotificationEntry + && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) { + return false; + } + List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry); + if (children != null) { + for (NotificationEntry child : children) { + if (child != entry && isHighPriority(child, allowImplicit)) { + return true; + } + } + } + return false; } - return false; } private boolean hasHighPriorityCharacteristics(NotificationEntry entry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java index 30386ab46382..ea369463da51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.render; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -38,6 +39,20 @@ public interface GroupExpansionManager { boolean isGroupExpanded(NotificationEntry entry); /** + * Whether the parent associated with this notification is expanded. + * If this notification is not part of a group or bundle, it will always return false. + */ + boolean isGroupExpanded(EntryAdapter entry); + + /** + * Set whether the group/bundle associated with this notification is expanded or not. + */ + void setGroupExpanded(EntryAdapter entry, boolean expanded); + + /** @return group/bundle expansion state after toggling. */ + boolean toggleGroupExpansion(EntryAdapter entry); + + /** * Set whether the group associated with this notification is expanded or not. */ void setGroupExpanded(NotificationEntry entry, boolean expanded); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index d1aff80b4e7c..16b98e20498a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -23,11 +23,13 @@ import androidx.annotation.NonNull; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.io.PrintWriter; import java.util.ArrayList; @@ -55,6 +57,8 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl */ private final Set<NotificationEntry> mExpandedGroups = new HashSet<>(); + private final Set<EntryAdapter> mExpandedCollections = new HashSet<>(); + @Inject public GroupExpansionManagerImpl(DumpManager dumpManager, GroupMembershipManager groupMembershipManager) { @@ -63,11 +67,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } /** - * Cleanup entries from mExpandedGroups that no longer exist in the pipeline. + * Cleanup entries from internal tracking that no longer exist in the pipeline. */ private final OnBeforeRenderListListener mNotifTracker = (entries) -> { - if (mExpandedGroups.isEmpty()) { - return; // nothing to do + if (NotificationBundleUi.isEnabled()) { + if (mExpandedCollections.isEmpty()) { + return; // nothing to do + } + } else { + if (mExpandedGroups.isEmpty()) { + return; // nothing to do + } } final Set<NotificationEntry> renderingSummaries = new HashSet<>(); @@ -77,10 +87,25 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } } - // If a group is in mExpandedGroups but not in the pipeline entries, collapse it. - final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries); - for (NotificationEntry entry : groupsToRemove) { - setGroupExpanded(entry, false); + if (NotificationBundleUi.isEnabled()) { + for (EntryAdapter entryAdapter : mExpandedCollections) { + boolean isInPipeline = false; + for (NotificationEntry entry : renderingSummaries) { + if (entry.getKey().equals(entryAdapter.getKey())) { + isInPipeline = true; + break; + } + } + if (!isInPipeline) { + setGroupExpanded(entryAdapter, false); + } + } + } else { + // If a group is in mExpandedGroups but not in the pipeline entries, collapse it. + final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries); + for (NotificationEntry entry : groupsToRemove) { + setGroupExpanded(entry, false); + } } }; @@ -96,11 +121,13 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public boolean isGroupExpanded(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); return mExpandedGroups.contains(mGroupMembershipManager.getGroupSummary(entry)); } @Override public void setGroupExpanded(NotificationEntry entry, boolean expanded) { + NotificationBundleUi.assertInLegacyMode(); NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); if (entry.getParent() == null) { if (expanded) { @@ -127,14 +154,61 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public boolean toggleGroupExpansion(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); + setGroupExpanded(entry, !isGroupExpanded(entry)); + return isGroupExpanded(entry); + } + + @Override + public boolean isGroupExpanded(EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry)); + } + + @Override + public void setGroupExpanded(EntryAdapter entry, boolean expanded) { + NotificationBundleUi.assertInNewMode(); + EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry); + if (!entry.isAttached()) { + if (expanded) { + Log.wtf(TAG, "Cannot expand group that is not attached"); + } else { + // The entry is no longer attached, but we still want to make sure we don't have + // a stale expansion state. + groupParent = entry; + } + } + + boolean changed; + if (expanded) { + changed = mExpandedCollections.add(groupParent); + } else { + changed = mExpandedCollections.remove(groupParent); + } + + // Only notify listeners if something changed. + if (changed) { + sendOnGroupExpandedChange(entry, expanded); + } + } + + @Override + public boolean toggleGroupExpansion(EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); setGroupExpanded(entry, !isGroupExpanded(entry)); return isGroupExpanded(entry); } @Override public void collapseGroups() { - for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) { - setGroupExpanded(entry, false); + if (NotificationBundleUi.isEnabled()) { + for (EntryAdapter entry : new ArrayList<>(mExpandedCollections)) { + setGroupExpanded(entry, false); + } + } else { + for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) { + setGroupExpanded(entry, false); + } } } @@ -145,9 +219,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl for (NotificationEntry entry : mExpandedGroups) { pw.println(" * " + entry.getKey()); } + pw.println(" mExpandedCollection: " + mExpandedCollections.size()); + for (EntryAdapter entry : mExpandedCollections) { + pw.println(" * " + entry.getKey()); + } } private void sendOnGroupExpandedChange(NotificationEntry entry, boolean expanded) { + NotificationBundleUi.assertInLegacyMode(); + for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) { + listener.onGroupExpansionChange(entry.getRow(), expanded); + } + } + + private void sendOnGroupExpandedChange(EntryAdapter entry, boolean expanded) { + NotificationBundleUi.assertInNewMode(); for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) { listener.onGroupExpansionChange(entry.getRow(), expanded); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java index 3158782e6fea..69267e5d9e55 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.render; import android.annotation.NonNull; import android.annotation.Nullable; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -29,6 +30,13 @@ import java.util.List; * generally assumes that the notification is attached (aka its parent is not null). */ public interface GroupMembershipManager { + + /** + * @return whether a given entry is the root (GroupEntry or BundleEntry) in a collection which + * has children + */ + boolean isGroupRoot(@NonNull EntryAdapter entry); + /** * @return whether a given notification is the summary in a group which has children */ @@ -42,16 +50,15 @@ public interface GroupMembershipManager { NotificationEntry getGroupSummary(@NonNull NotificationEntry entry); /** - * Similar to {@link #getGroupSummary(NotificationEntry)} but doesn't get the visual summary - * but the logical summary, i.e when a child is isolated, it still returns the summary as if - * it wasn't isolated. - * TODO: remove this when migrating to the new pipeline, this is taken care of in the - * dismissal logic built into NotifCollection + * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry + * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the + * BundleEntry will be returned. If the given notification is a group summary NotificationEntry, + * or a child of a group summary, the summary NotificationEntry will be returned, even if that + * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any + * group or bundle grouping, null will be returned. */ @Nullable - default NotificationEntry getLogicalGroupSummary(@NonNull NotificationEntry entry) { - return getGroupSummary(entry); - } + EntryAdapter getGroupRoot(@NonNull EntryAdapter entry); /** * @return whether a given notification is a child in a group @@ -59,9 +66,10 @@ public interface GroupMembershipManager { boolean isChildInGroup(@NonNull NotificationEntry entry); /** - * Whether this is the only child in a group + * @return whether a given notification is a child in a group. The group may be a notification + * group or a bundle. */ - boolean isOnlyChildInGroup(@NonNull NotificationEntry entry); + boolean isChildInGroup(@NonNull EntryAdapter entry); /** * Get the children that are in the summary's group, not including those isolated. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java index da1247953c4c..80a9f8adf8f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java @@ -22,9 +22,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.List; @@ -41,6 +43,7 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { @Override public boolean isGroupSummary(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry.getParent() == null) { // The entry is not attached, so it doesn't count. return false; @@ -49,33 +52,47 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { return entry.getParent().getSummary() == entry; } + @Override + public boolean isGroupRoot(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return entry == entry.getGroupRoot(); + } + @Nullable @Override public NotificationEntry getGroupSummary(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (isTopLevelEntry(entry) || entry.getParent() == null) { return null; } return entry.getParent().getSummary(); } + @Nullable + @Override + public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return entry.getGroupRoot(); + } + @Override public boolean isChildInGroup(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); // An entry is a child if it's not a summary or top level entry, but it is attached. return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null; } @Override - public boolean isOnlyChildInGroup(@NonNull NotificationEntry entry) { - if (entry.getParent() == null) { - return false; // The entry is not attached. - } - - return !isGroupSummary(entry) && entry.getParent().getChildren().size() == 1; + public boolean isChildInGroup(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + // An entry is a child if it's not a group root or top level entry, but it is attached. + return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry(); } @Nullable @Override public List<NotificationEntry> getChildren(@NonNull ListEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry instanceof GroupEntry) { return ((GroupEntry) entry).getChildren(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index be61dc95fe20..7d74a496853f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -46,6 +46,7 @@ import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; @@ -55,6 +56,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -118,7 +120,8 @@ public class HeadsUpManagerImpl @VisibleForTesting final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); private final HeadsUpManagerLogger mLogger; - private final int mMinimumDisplayTime; + private final int mMinimumDisplayTimeDefault; + private final int mMinimumDisplayTimeForUserInitiated; private final int mStickyForSomeTimeAutoDismissTime; private final int mAutoDismissTime; private final DelayableExecutor mExecutor; @@ -215,9 +218,11 @@ public class HeadsUpManagerImpl mGroupMembershipManager = groupMembershipManager; mVisualStabilityProvider = visualStabilityProvider; Resources resources = context.getResources(); - mMinimumDisplayTime = NotificationThrottleHun.isEnabled() + mMinimumDisplayTimeDefault = NotificationThrottleHun.isEnabled() ? resources.getInteger(R.integer.heads_up_notification_minimum_time_with_throttling) : resources.getInteger(R.integer.heads_up_notification_minimum_time); + mMinimumDisplayTimeForUserInitiated = resources.getInteger( + R.integer.heads_up_notification_minimum_time_for_user_initiated); mStickyForSomeTimeAutoDismissTime = resources.getInteger( R.integer.sticky_heads_up_notification_time); mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay); @@ -871,14 +876,24 @@ public class HeadsUpManagerImpl if (!hasPinnedHeadsUp() || topEntry == null) { return null; } else { + ExpandableNotificationRow topRow = topEntry.getRow(); if (topEntry.rowIsChildInGroup()) { - final NotificationEntry groupSummary = - mGroupMembershipManager.getGroupSummary(topEntry); - if (groupSummary != null) { - topEntry = groupSummary; + if (NotificationBundleUi.isEnabled()) { + final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot( + topRow.getEntryAdapter()); + if (adapter != null) { + topRow = adapter.getRow(); + } + } else { + final NotificationEntry groupSummary = + mGroupMembershipManager.getGroupSummary(topEntry); + if (groupSummary != null) { + topEntry = groupSummary; + topRow = topEntry.getRow(); + } } } - ExpandableNotificationRow topRow = topEntry.getRow(); + int[] tmpArray = new int[2]; topRow.getLocationOnScreen(tmpArray); int minX = tmpArray[0]; @@ -1358,7 +1373,12 @@ public class HeadsUpManagerImpl final long now = mSystemClock.elapsedRealtime(); if (updateEarliestRemovalTime) { - mEarliestRemovalTime = now + mMinimumDisplayTime; + if (StatusBarNotifChips.isEnabled() + && mPinnedStatus.getValue() == PinnedStatus.PinnedByUser) { + mEarliestRemovalTime = now + mMinimumDisplayTimeForUserInitiated; + } else { + mEarliestRemovalTime = now + mMinimumDisplayTimeDefault; + } } if (updatePostTime) { @@ -1377,7 +1397,7 @@ public class HeadsUpManagerImpl final long now = mSystemClock.elapsedRealtime(); return NotificationThrottleHun.isEnabled() ? Math.max(finishTime, mEarliestRemovalTime) - now - : Math.max(finishTime - now, mMinimumDisplayTime); + : Math.max(finishTime - now, mMinimumDisplayTimeDefault); }; scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt index 691f1f452da8..f755dbb48e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import javax.inject.Inject import kotlin.math.max @@ -112,14 +113,26 @@ class PeopleNotificationIdentifierImpl @Inject constructor( if (personExtractor.isPersonNotification(sbn)) TYPE_PERSON else TYPE_NON_PERSON private fun getPeopleTypeOfSummary(entry: NotificationEntry): Int { - if (!groupManager.isGroupSummary(entry)) { - return TYPE_NON_PERSON + if (NotificationBundleUi.isEnabled) { + if (!entry.sbn.notification.isGroupSummary) { + return TYPE_NON_PERSON; + } + + return getPeopleTypeForChildList(entry.parent?.children) + } else { + if (!groupManager.isGroupSummary(entry)) { + return TYPE_NON_PERSON + } + + return getPeopleTypeForChildList(groupManager.getChildren(entry)) } + } - val childTypes = groupManager.getChildren(entry) - ?.asSequence() - ?.map { getPeopleNotificationType(it) } - ?: return TYPE_NON_PERSON + private fun getPeopleTypeForChildList(children: List<NotificationEntry>?): Int { + val childTypes = children + ?.asSequence() + ?.map { getPeopleNotificationType(it) } + ?: return TYPE_NON_PERSON var groupType = TYPE_NON_PERSON for (childType in childTypes) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt index 393f95d3ad77..4bc685423659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.util.kotlin.FlowDumperImpl @@ -30,13 +29,12 @@ import kotlinx.coroutines.flow.map class AODPromotedNotificationInteractor @Inject constructor( - activeNotificationsInteractor: ActiveNotificationsInteractor, + promotedNotificationsInteractor: PromotedNotificationsInteractor, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { + /** The content to show as the promoted notification on AOD */ val content: Flow<PromotedNotificationContentModel?> = - activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs -> - notifs.firstNotNullOfOrNull { it.promotedContent } - } + promotedNotificationsInteractor.topPromotedNotificationContent val isPresent: Flow<Boolean> = content diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt new file mode 100644 index 000000000000..1015cfbefc41 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2025 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.notification.promoted.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor +import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * An interactor that provides details about promoted notification precedence, based on the + * presented order of current notification status bar chips. + */ +@SysUISingleton +class PromotedNotificationsInteractor +@Inject +constructor( + activeNotificationsInteractor: ActiveNotificationsInteractor, + callChipInteractor: CallChipInteractor, + notifChipsInteractor: StatusBarNotificationChipsInteractor, + @Background backgroundDispatcher: CoroutineDispatcher, +) { + /** + * This is the ordered list of notifications (and the promoted content) represented as chips in + * the status bar. + */ + private val orderedChipNotifications: Flow<List<NotifAndPromotedContent>> = + combine(callChipInteractor.ongoingCallState, notifChipsInteractor.allNotificationChips) { + callState, + notifChips -> + buildList { + val callData = callState.getNotifData()?.also { add(it) } + addAll( + notifChips.mapNotNull { + when (it.key) { + callData?.key -> null // do not re-add the same call + else -> NotifAndPromotedContent(it.key, it.promotedContent) + } + } + ) + } + } + + private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? = + when (this) { + is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent) + is OngoingCallModel.InCallWithVisibleApp -> + // TODO(b/395989259): support InCallWithVisibleApp when it has notif data + null + is OngoingCallModel.NoCall -> null + } + + /** + * The top promoted notification represented by a chip, with the order determined by the order + * of the chips, not the notifications. + */ + private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> = + orderedChipNotifications + .map { list -> list.firstNotNullOfOrNull { it.promotedContent } } + .distinctUntilNewInstance() + + /** This is the top-most promoted notification, which should avoid regular changing. */ + val topPromotedNotificationContent: Flow<PromotedNotificationContentModel?> = + combine( + topPromotedChipNotification, + activeNotificationsInteractor.topLevelRepresentativeNotifications, + ) { topChipNotif, topLevelNotifs -> + topChipNotif ?: topLevelNotifs.firstNotNullOfOrNull { it.promotedContent } + } + // #equals() can be a bit expensive on this object, but this flow will regularly try to + // emit the same immutable instance over and over, so just prevent that. + .distinctUntilNewInstance() + + /** + * This is the ordered list of notifications (and the promoted content) represented as chips in + * the status bar. Flows on the background context. + */ + val orderedChipNotificationKeys: Flow<List<String>> = + orderedChipNotifications + .map { list -> list.map { it.key } } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + /** + * Returns flow where all subsequent repetitions of the same object instance are filtered out. + */ + private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b } + + /** + * A custom pair, but providing clearer semantic names, and implementing equality as being the + * same instance of the promoted content model, which allows us to use distinctUntilChanged() on + * flows containing this without doing pixel comparisons on the Bitmaps inside Icon objects + * provided by the Notification. + */ + private data class NotifAndPromotedContent( + val key: String, + val promotedContent: PromotedNotificationContentModel?, + ) { + /** + * Define the equals of this object to only check the reference equality of the promoted + * content so that we can mark. + */ + override fun equals(other: Any?): Boolean { + return when { + other == null -> false + other === this -> true + other !is NotifAndPromotedContent -> return false + else -> key == other.key && promotedContent === other.promotedContent + } + } + + /** Define the hashCode to be very quick, even if it increases collisions. */ + override fun hashCode(): Int { + var result = key.hashCode() + result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0) + return result + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index fd334447e13a..987a67a90e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -105,6 +105,7 @@ import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -120,6 +121,7 @@ import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedac import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.shared.TransparentHeaderFix; import com.android.systemui.statusbar.notification.stack.AmbientState; @@ -268,6 +270,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private String mLoggingKey; private NotificationGuts mGuts; private NotificationEntry mEntry; + private EntryAdapter mEntryAdapter; private String mAppName; private NotificationRebindingTracker mRebindingTracker; private FalsingManager mFalsingManager; @@ -390,11 +393,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) { - if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) - && mGroupMembershipManager.isGroupSummary(mEntry)) { + boolean isGroupRoot = NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry); + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) { mGroupExpansionChanging = true; - final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); - boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry); + final boolean wasExpanded = NotificationBundleUi.isEnabled() + ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter) + : mGroupExpansionManager.isGroupExpanded(mEntry); + boolean nowExpanded = NotificationBundleUi.isEnabled() + ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter) + : mGroupExpansionManager.toggleGroupExpansion(mEntry); mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded); if (shouldLogExpandClickMetric) { mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded); @@ -910,6 +919,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mEntry; } + @Nullable + public EntryAdapter getEntryAdapter() { + NotificationBundleUi.assertInNewMode(); + return mEntryAdapter; + } + @Override public boolean isHeadsUp() { return mIsHeadsUp; @@ -2010,11 +2025,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * * @param context context context of the view * @param attrs attributes used to initialize parent view + * @param user the user the row is associated to + */ + public ExpandableNotificationRow(Context context, AttributeSet attrs, UserHandle user) { + this(context, attrs, userContextForEntry(context, user)); + NotificationBundleUi.assertInNewMode(); + } + + /** + * Constructs an ExpandableNotificationRow. Used by layout inflation (with a custom {@code + * AsyncLayoutFactory} in {@link RowInflaterTask}. + * + * @param context context context of the view + * @param attrs attributes used to initialize parent view * @param entry notification that the row will be associated to (determines the user for the * ImageResolver) */ public ExpandableNotificationRow(Context context, AttributeSet attrs, NotificationEntry entry) { this(context, attrs, userContextForEntry(context, entry)); + NotificationBundleUi.assertInLegacyMode(); } private static Context userContextForEntry(Context base, NotificationEntry entry) { @@ -2025,6 +2054,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView UserHandle.of(entry.getSbn().getNormalizedUserId()), /* flags= */ 0); } + private static Context userContextForEntry(Context base, UserHandle user) { + if (base.getUserId() == user.getIdentifier()) { + return base; + } + return base.createContextAsUser(user, /* flags= */ 0); + } + private ExpandableNotificationRow(Context sysUiContext, AttributeSet attrs, Context userContext) { super(sysUiContext, attrs); @@ -2067,7 +2103,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView IStatusBarService statusBarService, UiEventLogger uiEventLogger, NotificationRebindingTracker notificationRebindingTracker) { - mEntry = entry; + + if (NotificationBundleUi.isEnabled()) { + // TODO (b/395857098): remove when all usages are migrated + mEntryAdapter = entry.getEntryAdapter(); + mEntry = entry; + } else { + mEntry = entry; + } mAppName = appName; mRebindingTracker = notificationRebindingTracker; if (mMenuRow == null) { @@ -2876,7 +2919,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion && !mChildrenContainer.showingAsLowPriority()) { - final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); + final boolean wasExpanded = isGroupExpanded(); mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded); onExpansionChanged(true /* userAction */, wasExpanded); return; @@ -3031,6 +3074,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public boolean isGroupExpanded() { + if (NotificationBundleUi.isEnabled()) { + return mGroupExpansionManager.isGroupExpanded(mEntryAdapter); + } return mGroupExpansionManager.isGroupExpanded(mEntry); } @@ -3769,7 +3815,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void onExpandedByGesture(boolean userExpanded) { int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER; - if (mGroupMembershipManager.isGroupSummary(mEntry)) { + if (NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry)) { event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER; } mMetricsLogger.action(event, userExpanded); @@ -3805,7 +3853,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { - nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); + nowExpanded = isGroupExpanded(); } // Note: nowExpanded is going to be true here on the first expansion of minimized groups, // even though the group itself is not expanded. Use mGroupExpansionManager to get the real diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index e311b53bfa64..0c1dd2e026b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -56,6 +56,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; @@ -1457,12 +1458,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } @Override - public void handleInflationException(NotificationEntry entry, Exception e) { + public void handleInflationException(Exception e) { handleError(e); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { mEntry.onInflationTaskFinished(); mRow.onNotificationUpdated(); if (mCallback != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 0be1d5d9d79d..05934e7edfba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.lang.annotation.Retention; @@ -170,13 +171,29 @@ public interface NotificationRowContentBinder { * @param entry notification which failed to inflate content * @param e exception */ - void handleInflationException(NotificationEntry entry, Exception e); + default void handleInflationException(NotificationEntry entry, Exception e) { + handleInflationException(e); + } + + /** + * Callback for when there is an inflation exception + * + * @param e exception + */ + void handleInflationException(Exception e); /** * Callback for after the content views finish inflating. * * @param entry the entry with the content views set */ - void onAsyncInflationFinished(NotificationEntry entry); + default void onAsyncInflationFinished(NotificationEntry entry) { + onAsyncInflationFinished(); + } + + /** + * Callback for after the content views finish inflating. + */ + void onAsyncInflationFinished(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 517fc3a06d84..761d3fe91cd0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.NmSummarizationUiFlag +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel @@ -76,6 +77,7 @@ import com.android.systemui.statusbar.notification.row.shared.NotificationConten import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer import com.android.systemui.statusbar.policy.InflatedSmartReplyState import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder @@ -536,7 +538,7 @@ constructor( val ident: String = (sbn.packageName + "/0x" + Integer.toHexString(sbn.id)) Log.e(TAG, "couldn't inflate view for notification $ident", e) callback?.handleInflationException( - row.entry, + if (NotificationBundleUi.isEnabled) entry else row.entry, InflationException("Couldn't inflate contentViews$e"), ) @@ -554,11 +556,11 @@ constructor( logger.logAsyncTaskProgress(entry, "aborted") } - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { handleError(e) } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { this.entry.onInflationTaskFinished() row.onNotificationUpdated() callback?.onAsyncInflationFinished(this.entry) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index 6883ec575d7e..da361406fa2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -21,10 +21,12 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import androidx.annotation.NonNull; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import javax.inject.Inject; @@ -52,7 +54,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @Override protected void executeStage( - @NonNull NotificationEntry entry, + final @NonNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @NonNull StageCallback callback) { RowContentBindParams params = getStageParams(entry); @@ -77,15 +79,35 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { InflationCallback inflationCallback = new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, Exception e) { - mNotifInflationErrorManager.setInflationError(entry, e); + public void handleInflationException(NotificationEntry errorEntry, Exception e) { + if (NotificationBundleUi.isEnabled()) { + mNotifInflationErrorManager.setInflationError(entry, e); + } else { + mNotifInflationErrorManager.setInflationError(errorEntry, e); + } + } + + @Override + public void handleInflationException(Exception e) { + } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { - mNotifInflationErrorManager.clearInflationError(entry); - getStageParams(entry).clearDirtyContentViews(); - callback.onStageFinished(entry); + public void onAsyncInflationFinished(NotificationEntry finishedEntry) { + if (NotificationBundleUi.isEnabled()) { + mNotifInflationErrorManager.clearInflationError(entry); + getStageParams(entry).clearDirtyContentViews(); + callback.onStageFinished(entry); + } else { + mNotifInflationErrorManager.clearInflationError(finishedEntry); + getStageParams(finishedEntry).clearDirtyContentViews(); + callback.onStageFinished(finishedEntry); + } + } + + @Override + public void onAsyncInflationFinished() { + } }; mBinder.cancelBind(entry, row); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java index 9f634bef4c5e..d60e37423e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import android.content.Context; +import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -30,8 +31,10 @@ import androidx.asynclayoutinflater.view.AsyncLayoutFactory; import androidx.asynclayoutinflater.view.AsyncLayoutInflater; import com.android.systemui.res.R; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.SystemClock; import java.util.concurrent.Executor; @@ -53,11 +56,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private final SystemClock mSystemClock; private final RowInflaterTaskLogger mLogger; private long mInflateStartTimeMs; + private UserTracker mUserTracker; @Inject - public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) { + public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger, + UserTracker userTracker) { mSystemClock = systemClock; mLogger = logger; + mUserTracker = userTracker; } /** @@ -107,7 +113,8 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) { - return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger); + return new RowAsyncLayoutInflater( + entry, mSystemClock, mLogger, mUserTracker.getUserHandle()); } /** @@ -148,12 +155,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private final NotificationEntry mEntry; private final SystemClock mSystemClock; private final RowInflaterTaskLogger mLogger; + private final UserHandle mTargetUser; public RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock, - RowInflaterTaskLogger logger) { + RowInflaterTaskLogger logger, UserHandle targetUser) { mEntry = entry; mSystemClock = systemClock; mLogger = logger; + mTargetUser = targetUser; } @Nullable @@ -165,8 +174,12 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } final long startMs = mSystemClock.elapsedRealtime(); - final ExpandableNotificationRow row = - new ExpandableNotificationRow(context, attrs, mEntry); + ExpandableNotificationRow row = null; + if (NotificationBundleUi.isEnabled()) { + row = new ExpandableNotificationRow(context, attrs, mTargetUser); + } else { + row = new ExpandableNotificationRow(context, attrs, mEntry); + } final long elapsedMs = mSystemClock.elapsedRealtime() - startMs; mLogger.logCreatedRow(mEntry, elapsedMs); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index b9352bf64be4..3ee827332877 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -122,6 +122,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; @@ -2958,9 +2959,13 @@ public class NotificationStackScrollLayout } private boolean isChildInGroup(View child) { - return child instanceof ExpandableNotificationRow - && mGroupMembershipManager.isChildInGroup( - ((ExpandableNotificationRow) child).getEntry()); + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow childRow = (ExpandableNotificationRow) child; + return NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isChildInGroup(childRow.getEntryAdapter()) + : mGroupMembershipManager.isChildInGroup(childRow.getEntry()); + } + return false; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 6385d53dbc8b..10b665d8ef01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -25,7 +25,7 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.nano.MetricsProto import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.repeatWhenAttachedToWindow import com.android.systemui.plugins.FalsingManager @@ -76,7 +76,7 @@ import kotlinx.coroutines.flow.stateIn class NotificationListViewBinder @Inject constructor( - @Background private val backgroundDispatcher: CoroutineDispatcher, + @NotifInflation private val inflationDispatcher: CoroutineDispatcher, private val hiderTracker: DisplaySwitchNotificationsHiderTracker, @ShadeDisplayAware private val configuration: ConfigurationState, private val falsingManager: FalsingManager, @@ -155,7 +155,7 @@ constructor( parentView, attachToRoot = false, ) - .flowOn(backgroundDispatcher) + .flowOn(inflationDispatcher) .collectLatest { footerView: FooterView -> traceAsync("bind FooterView") { parentView.setFooterView(footerView) @@ -240,7 +240,7 @@ constructor( parentView, attachToRoot = false, ) - .flowOn(backgroundDispatcher) + .flowOn(inflationDispatcher) .collectLatest { emptyShadeView: EmptyShadeView -> traceAsync("bind EmptyShadeView") { parentView.setEmptyShadeView(emptyShadeView) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 1dc9de489806..05a46cd9fa31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.kotlin.JavaAdapter; @@ -215,7 +216,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, if (ExpandHeadsUpOnInlineReply.isEnabled()) { if (row.isChildInGroup() && !row.areChildrenExpanded()) { // The group isn't expanded, let's make sure it's visible! - mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); + } else { + mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + } } else if (!row.isChildInGroup()) { final boolean expandNotification; if (row.isPinned()) { @@ -233,7 +238,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, } else { if (row.isChildInGroup() && !row.areChildrenExpanded()) { // The group isn't expanded, let's make sure it's visible! - mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); + } else { + mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + } } if (android.app.Flags.compactHeadsUpNotificationReply() diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 28cf78f6777e..9f60fe212567 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -18,8 +18,10 @@ package com.android.systemui.theme; import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8; +import static com.android.systemui.Flags.hardwareColorStyles; import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; +import static com.android.systemui.monet.ColorScheme.GOOGLE_BLUE; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET; @@ -73,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; @@ -99,6 +102,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -136,9 +140,11 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final DeviceProvisionedController mDeviceProvisionedController; private final Resources mResources; // Current wallpaper colors associated to a user. - private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); + @VisibleForTesting + protected final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); private final WallpaperManager mWallpaperManager; private final ActivityManager mActivityManager; + protected final SystemPropertiesHelper mSystemPropertiesHelper; @VisibleForTesting protected ColorScheme mColorScheme; // If fabricated overlays were already created for the current theme. @@ -423,7 +429,9 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { JavaAdapter javaAdapter, KeyguardTransitionInteractor keyguardTransitionInteractor, UiModeManager uiModeManager, - ActivityManager activityManager) { + ActivityManager activityManager, + SystemPropertiesHelper systemPropertiesHelper + ) { mContext = context; mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY); @@ -443,6 +451,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mKeyguardTransitionInteractor = keyguardTransitionInteractor; mUiModeManager = uiModeManager; mActivityManager = activityManager; + mSystemPropertiesHelper = systemPropertiesHelper; dumpManager.registerDumpable(TAG, this); Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor @@ -498,29 +507,38 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); + WallpaperColors systemColor; + if (hardwareColorStyles() && !mDeviceProvisionedController.isCurrentUserSetup()) { + Pair<Integer, Color> defaultSettings = getThemeSettingsDefaults(); + mThemeStyle = defaultSettings.first; + Color seedColor = defaultSettings.second; + + // we only use the first color anyway, so we can pass only the single color we have + systemColor = new WallpaperColors( + /*primaryColor*/ seedColor, + /*secondaryColor*/ seedColor, + /*tertiaryColor*/ seedColor + ); + } else { + systemColor = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + } + // Upon boot, make sure we have the most up to date colors Runnable updateColors = () -> { - WallpaperColors systemColor = mWallpaperManager.getWallpaperColors( - getDefaultWallpaperColorsSource(mUserTracker.getUserId())); - Runnable applyColors = () -> { - if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); - mCurrentColors.put(mUserTracker.getUserId(), systemColor); - reevaluateSystemTheme(false /* forceReload */); - }; - if (mDeviceProvisionedController.isCurrentUserSetup()) { - mMainExecutor.execute(applyColors); - } else { - applyColors.run(); - } + if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); + mCurrentColors.put(mUserTracker.getUserId(), systemColor); + reevaluateSystemTheme(false /* forceReload */); }; // Whenever we're going directly to setup wizard, we need to process colors synchronously, // otherwise we'll see some jank when the activity is recreated. if (!mDeviceProvisionedController.isCurrentUserSetup()) { - updateColors.run(); + mMainExecutor.execute(updateColors); } else { mBgExecutor.execute(updateColors); } + mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null, UserHandle.USER_ALL); @@ -604,7 +622,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { @VisibleForTesting protected boolean isPrivateProfile(UserHandle userHandle) { - Context usercontext = mContext.createContextAsUser(userHandle,0); + Context usercontext = mContext.createContextAsUser(userHandle, 0); return usercontext.getSystemService(UserManager.class).isPrivateProfile(); } @@ -720,6 +738,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return true; } + @SuppressWarnings("StringCaseLocaleUsage") // Package name is not localized private void updateThemeOverlays() { final int currentUser = mUserTracker.getUserId(); final String overlayPackageJson = mSecureSettings.getStringForUser( @@ -746,7 +765,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE); if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) { try { - String colorString = systemPalette.getPackageName().toLowerCase(); + String colorString = systemPalette.getPackageName().toLowerCase(); if (!colorString.startsWith("#")) { colorString = "#" + colorString; } @@ -856,6 +875,75 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return style; } + protected Pair<Integer, String> getHardwareColorSetting() { + String deviceColorProperty = "ro.boot.hardware.color"; + + String[] themeData = mResources.getStringArray( + com.android.internal.R.array.theming_defaults); + + // Color can be hex (`#FF0000`) or `home_wallpaper` + Map<String, Pair<Integer, String>> themeMap = new HashMap<>(); + + // extract all theme settings + for (String themeEntry : themeData) { + String[] themeComponents = themeEntry.split("\\|"); + if (themeComponents.length != 3) continue; + themeMap.put(themeComponents[0], + new Pair<>(Style.valueOf(themeComponents[1]), themeComponents[2])); + } + + Pair<Integer, String> fallbackTheme = themeMap.get("*"); + if (fallbackTheme == null) { + Log.d(TAG, "Theming wildcard not found. Fallback to TONAL_SPOT|" + COLOR_SOURCE_HOME); + fallbackTheme = new Pair<>(Style.TONAL_SPOT, COLOR_SOURCE_HOME); + } + + String deviceColorPropertyValue = mSystemPropertiesHelper.get(deviceColorProperty); + Pair<Integer, String> selectedTheme = themeMap.get(deviceColorPropertyValue); + if (selectedTheme == null) { + Log.d(TAG, "Sysprop `" + deviceColorProperty + "` of value '" + deviceColorPropertyValue + + "' not found in theming_defaults: " + Arrays.toString(themeData)); + selectedTheme = fallbackTheme; + } + + return selectedTheme; + } + + @VisibleForTesting + protected Pair<Integer, Color> getThemeSettingsDefaults() { + + Pair<Integer, String> selectedTheme = getHardwareColorSetting(); + + // Last fallback color + Color defaultSeedColor = Color.valueOf(GOOGLE_BLUE); + + // defaultColor will come from wallpaper or be parsed from a string + boolean isWallpaper = selectedTheme.second.equals(COLOR_SOURCE_HOME); + + if (isWallpaper) { + WallpaperColors wallpaperColors = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + + if (wallpaperColors != null) { + defaultSeedColor = wallpaperColors.getPrimaryColor(); + } + + Log.d(TAG, "Default seed color read from home wallpaper: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } else { + try { + defaultSeedColor = Color.valueOf(Color.parseColor(selectedTheme.second)); + Log.d(TAG, "Default seed color read from resource: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error parsing color: " + selectedTheme.second, e); + // defaultSeedColor remains unchanged in this case + } + } + + return new Pair<>(selectedTheme.first, defaultSeedColor); + } + @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mSystemColors=" + mCurrentColors); diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt index e5c1e7daa25a..79ff38eabc08 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.coroutines.newTracingContext import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.util.settings.SettingsSingleThreadBackground import dagger.Module @@ -123,4 +124,19 @@ class SysUICoroutinesModule { ): CoroutineContext { return uiBgCoroutineDispatcher } + + /** Coroutine dispatcher for background notification inflation. */ + @Provides + @NotifInflation + @SysUISingleton + fun notifInflationCoroutineDispatcher( + @NotifInflation notifInflationExecutor: Executor, + @Background bgCoroutineDispatcher: CoroutineDispatcher, + ): CoroutineDispatcher { + if (com.android.systemui.Flags.useNotifInflationThreadForFooter()) { + return notifInflationExecutor.asCoroutineDispatcher() + } else { + return bgCoroutineDispatcher + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 281ce16b539f..19d1224a9bf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -28,6 +28,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -46,6 +48,7 @@ import android.os.Bundle; import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; @@ -59,9 +62,12 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -83,6 +89,9 @@ public class NotificationEntryTest extends SysuiTestCase { private NotificationChannel mChannel = Mockito.mock(NotificationChannel.class); private final FakeSystemClock mClock = new FakeSystemClock(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setup() { Notification.Builder n = new Notification.Builder(mContext, "") @@ -444,6 +453,145 @@ public class NotificationEntryTest extends SysuiTestCase { // no crash, good } + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getParent_adapter() { + GroupEntry ge = new GroupEntryBuilder() + .build(); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(ge) + .build(); + + assertThat(entry.getEntryAdapter().getParent()).isEqualTo(entry.getParent()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void isTopLevelEntry_adapter() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + + assertThat(entry.getEntryAdapter().isTopLevelEntry()).isTrue(); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getKey_adapter() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + + assertThat(entry.getEntryAdapter().getKey()).isEqualTo(entry.getKey()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getRow_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getRow()).isEqualTo(entry.getRow()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getGroupRoot_adapter_groupSummary() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getGroupRoot()).isNull(); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getGroupRoot_adapter_groupChild() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build(); + + NotificationEntry parent = new NotificationEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + GroupEntryBuilder groupEntry = new GroupEntryBuilder() + .setSummary(parent); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(groupEntry.build()) + .build(); + + assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter()); + } private Notification.Action createContextualAction(String title) { return new Notification.Action.Builder( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 699e8c30afde..47238fedee4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -23,6 +23,7 @@ import android.service.notification.StatusBarNotification import android.testing.TestableLooper import android.testing.ViewUtils import android.view.NotificationHeaderView +import android.view.NotificationTopLineView import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -37,6 +38,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.FeedbackIcon import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -82,8 +84,21 @@ class NotificationContentViewTest : SysuiTestCase() { val mockEntry = createMockNotificationEntry() row = spy( - ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply { - entry = mockEntry + when (NotificationBundleUi.isEnabled) { + true -> { + ExpandableNotificationRow( + mContext, + /* attrs= */ null, + UserHandle.CURRENT + ).apply { + entry = mockEntry + } + } + false -> { + ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply { + entry = mockEntry + } + } } ) ViewUtils.attachView(fakeParent) @@ -270,7 +285,7 @@ class NotificationContentViewTest : SysuiTestCase() { val icon = FeedbackIcon( R.drawable.ic_feedback_alerted, - R.string.notification_feedback_indicator_alerted + R.string.notification_feedback_indicator_alerted, ) view.setFeedbackIcon(icon) @@ -291,10 +306,7 @@ class NotificationContentViewTest : SysuiTestCase() { val mockHeadsUpEB = mock<NotificationExpandButton>() val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB) - val view = - createContentView( - isSystemExpanded = false, - ) + val view = createContentView(isSystemExpanded = false) // Update all 3 child forms view.apply { @@ -319,12 +331,14 @@ class NotificationContentViewTest : SysuiTestCase() { private fun createMockNotificationHeaderView( height: Int, - mockExpandedEB: NotificationExpandButton + mockExpandedEB: NotificationExpandButton, ) = spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height }) .apply { whenever(this.animate()).thenReturn(mock()) whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB) + whenever(this.findViewById<View>(R.id.notification_top_line)) + .thenReturn(mock<NotificationTopLineView>()) } @Test @@ -344,7 +358,7 @@ class NotificationContentViewTest : SysuiTestCase() { isSystemExpanded = false, contractedView = mockContracted, expandedView = mockExpanded, - headsUpView = mockHeadsUp + headsUpView = mockHeadsUp, ) view.setRemoteInputVisible(true) @@ -373,7 +387,7 @@ class NotificationContentViewTest : SysuiTestCase() { isSystemExpanded = false, contractedView = mockContracted, expandedView = mockExpanded, - headsUpView = mockHeadsUp + headsUpView = mockHeadsUp, ) view.setRemoteInputVisible(false) @@ -635,7 +649,7 @@ class NotificationContentViewTest : SysuiTestCase() { contractedView: View = createViewWithHeight(contractedHeight), expandedView: View = createViewWithHeight(expandedHeight), headsUpView: View = createViewWithHeight(contractedHeight), - row: ExpandableNotificationRow = this.row + row: ExpandableNotificationRow = this.row, ): NotificationContentView { val height = if (isSystemExpanded) expandedHeight else contractedHeight doReturn(height).whenever(row).intrinsicHeight @@ -647,7 +661,7 @@ class NotificationContentViewTest : SysuiTestCase() { setHeights( /* smallHeight= */ contractedHeight, /* headsUpMaxHeight= */ contractedHeight, - /* maxHeight= */ expandedHeight + /* maxHeight= */ expandedHeight, ) contractedChild = contractedView expandedChild = expandedView diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 14a1233045bb..10886760b521 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -63,6 +63,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; import com.android.systemui.dock.DockManager; +import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository; @@ -118,10 +119,8 @@ public class ScrimControllerTest extends SysuiTestCase { @Rule public Expect mExpect = Expect.create(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private final FakeConfigurationController mConfigurationController = - new FakeConfigurationController(); - private final LargeScreenShadeInterpolator - mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); + private FakeConfigurationController mConfigurationController; + private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator; private final TestScope mTestScope = mKosmos.getTestScope(); private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); @@ -137,6 +136,7 @@ public class ScrimControllerTest extends SysuiTestCase { private boolean mAlwaysOnEnabled; private TestableLooper mLooper; private Context mContext; + @Mock private DozeParameters mDozeParameters; @Mock private LightBarController mLightBarController; @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory; @@ -149,12 +149,11 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; @Mock private AlternateBouncerToGoneTransitionViewModel mAlternateBouncerToGoneTransitionViewModel; - private final KeyguardTransitionInteractor mKeyguardTransitionInteractor = - mKosmos.getKeyguardTransitionInteractor(); - private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository = - mKosmos.getKeyguardTransitionRepository(); @Mock private KeyguardInteractor mKeyguardInteractor; + private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private FakeKeyguardTransitionRepository mKeyguardTransitionRepository; + // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @@ -238,6 +237,9 @@ public class ScrimControllerTest extends SysuiTestCase { when(mContext.getColor(com.android.internal.R.color.materialColorSurface)) .thenAnswer(invocation -> mSurfaceColor); + mConfigurationController = new FakeConfigurationController(); + mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); + mScrimBehind = spy(new ScrimView(mContext)); mScrimInFront = new ScrimView(mContext); mNotificationsScrim = new ScrimView(mContext); @@ -270,6 +272,9 @@ public class ScrimControllerTest extends SysuiTestCase { when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha()) .thenReturn(emptyFlow()); + mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository(); + mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor(); + mScrimController = new ScrimController( mLightBarController, mDozeParameters, @@ -322,6 +327,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -337,6 +343,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToShadeLocked() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -373,6 +380,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToShadeLocked_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -391,6 +399,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToOff() { mScrimController.legacyTransitionTo(ScrimState.OFF); finishAnimationsImmediately(); @@ -406,6 +415,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_withRegularWallpaper() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -421,6 +431,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_withFrontAlphaUpdates() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -465,6 +476,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_afterDocked_ignoresAlwaysOnAndUpdatesFrontAlpha() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -506,6 +518,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToPulsing_withFrontAlphaUpdates() { // Pre-condition // Need to go to AoD first because PULSING doesn't change @@ -551,6 +564,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguardBouncer() { mScrimController.legacyTransitionTo(BOUNCER); finishAnimationsImmediately(); @@ -571,6 +585,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void lockscreenToHubTransition_setsBehindScrimAlpha() { // Start on lockscreen. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -617,6 +632,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void hubToLockscreenTransition_setsViewAlpha() { // Start on glanceable hub. mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -663,6 +679,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToHub() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -677,6 +694,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openBouncerOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -706,6 +724,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openShadeOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -734,6 +753,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToHubOverDream() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -748,6 +768,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openBouncerOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -777,6 +798,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openShadeOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -805,6 +827,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() { assertEquals(BOUNCER.getBehindTint(), 0x112233); mSurfaceColor = 0x223344; @@ -813,6 +836,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -825,6 +849,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguardBouncer_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -845,6 +870,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -867,6 +893,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(BOUNCER); @@ -889,6 +916,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToBouncer() { mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED); finishAnimationsImmediately(); @@ -902,6 +930,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlocked_clippedQs() { mScrimController.setClipsQsScrim(true); mScrimController.setRawPanelExpansionFraction(0f); @@ -960,6 +989,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() { mScrimController.setClipsQsScrim(false); mScrimController.setRawPanelExpansionFraction(0f); @@ -999,6 +1029,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimStateCallback() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1014,6 +1045,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void panelExpansion() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1036,6 +1068,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion() { reset(mScrimBehind); mScrimController.setQsPosition(1f, 999 /* value doesn't matter */); @@ -1048,6 +1081,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1061,6 +1095,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_half_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1074,6 +1109,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void panelExpansionAffectsAlpha() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1096,6 +1132,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlockedFromOff() { // Simulate unlock with fingerprint without AOD mScrimController.legacyTransitionTo(ScrimState.OFF); @@ -1118,6 +1155,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlockedFromAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1140,6 +1178,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimBlanksBeforeLeavingAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1163,6 +1202,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimBlankCallbackWhenUnlockingFromPulse() { boolean[] blanked = {false}; // Simulate unlock with fingerprint @@ -1181,6 +1221,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void blankingNotRequired_leavingAoD() { // GIVEN display does NOT need blanking when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); @@ -1236,6 +1277,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimCallback() { int[] callOrder = {0, 0, 0}; int[] currentCall = {0}; @@ -1262,12 +1304,14 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimCallbacksWithoutAmbientDisplay() { mAlwaysOnEnabled = false; testScrimCallback(); } @Test + @DisableSceneContainer public void testScrimCallbackCancelled() { boolean[] cancelledCalled = {false}; mScrimController.legacyTransitionTo(ScrimState.AOD, new ScrimController.Callback() { @@ -1281,6 +1325,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testHoldsWakeLock_whenAOD() { mScrimController.legacyTransitionTo(ScrimState.AOD); verify(mWakeLock).acquire(anyString()); @@ -1290,6 +1335,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoesNotHoldWakeLock_whenUnlocking() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1297,6 +1343,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testCallbackInvokedOnSameStateTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1306,6 +1353,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testConservesExpansionOpacityAfterTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1323,6 +1371,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testCancelsOldAnimationBeforeBlanking() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -1335,6 +1384,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsAreNotFocusable() { assertFalse("Behind scrim should not be focusable", mScrimBehind.isFocusable()); assertFalse("Front scrim should not be focusable", mScrimInFront.isFocusable()); @@ -1343,6 +1393,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testEatsTouchEvent() { HashSet<ScrimState> eatsTouches = new HashSet<>(Collections.singletonList(ScrimState.AOD)); @@ -1359,6 +1410,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testAnimatesTransitionToAod() { when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); ScrimState.AOD.prepare(ScrimState.KEYGUARD); @@ -1373,6 +1425,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testIsLowPowerMode() { HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList( ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING)); @@ -1390,6 +1443,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsOpaque_whenShadeFullyExpanded() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(1); @@ -1404,6 +1458,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisible() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1419,6 +1474,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoesntAnimate_whenUnlocking() { // LightRevealScrim will animate the transition, we should only hide the keyguard scrims. ScrimState.UNLOCKED.prepare(ScrimState.KEYGUARD); @@ -1439,6 +1495,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisible_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1454,6 +1511,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisibleOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); mScrimController.setQsPosition(0.25f, 300); @@ -1465,6 +1523,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationScrimTransparent_whenOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); // even if shade is not pulled down, panel has expansion of 1 on the lockscreen @@ -1477,6 +1536,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() { mScrimController.setRawPanelExpansionFraction(1); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -1488,6 +1548,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); // clipping doesn't change tested logic but allows to assert scrims more in line with @@ -1504,6 +1565,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); @@ -1520,6 +1582,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1535,6 +1598,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1554,6 +1618,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); mScrimController.setClipsQsScrim(true); @@ -1574,6 +1639,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(true); @@ -1594,6 +1660,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(false); @@ -1605,6 +1672,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationTransparency_followsTransitionToFullShade() { mScrimController.setClipsQsScrim(true); @@ -1646,6 +1714,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationTransparency_followsNotificationScrimProgress() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setRawPanelExpansionFraction(1.0f); @@ -1662,6 +1731,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1697,6 +1767,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() { int overScrollAmount = 10; @@ -1706,6 +1777,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() { int overScrollAmount = 10; @@ -1715,6 +1787,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() { int overScrollAmount = 10; @@ -1724,6 +1797,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationBoundsTopGetsPassedToKeyguard() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -1734,6 +1808,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1744,6 +1819,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToDreaming() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -1763,6 +1839,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void keyguardGoingAwayUpdateScrims() { when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); mScrimController.updateScrims(); @@ -1772,6 +1849,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Test + @DisableSceneContainer public void setUnOccludingAnimationKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -1786,6 +1864,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testHidesScrimFlickerInActivity() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1804,6 +1883,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1813,6 +1893,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void aodStateSetsFrontScrimToNotBlend() { mScrimController.legacyTransitionTo(ScrimState.AOD); assertFalse("Front scrim should not blend with main color", @@ -1820,6 +1901,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void applyState_unlocked_bouncerShowing() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setBouncerHiddenFraction(0.99f); @@ -1829,6 +1911,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void ignoreTransitionRequestWhileKeyguardTransitionRunning() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.mBouncerToGoneTransition.accept( @@ -1841,6 +1924,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() { when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); mScrimController.mBouncerToGoneTransition.accept( @@ -1851,6 +1935,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsLightBarController() { reset(mLightBarController); mScrimController.mBouncerToGoneTransition.accept( @@ -1862,6 +1947,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoNotAnimateChangeIfOccludeAnimationPlaying() { mScrimController.setOccludeAnimationPlaying(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1870,6 +1956,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true); @@ -1942,9 +2029,9 @@ public class ScrimControllerTest extends SysuiTestCase { // Check combined scrim visibility. final int visibility; - if (scrimToAlpha.values().contains(OPAQUE)) { + if (scrimToAlpha.containsValue(OPAQUE)) { visibility = OPAQUE; - } else if (scrimToAlpha.values().contains(SEMI_TRANSPARENT)) { + } else if (scrimToAlpha.containsValue(SEMI_TRANSPARENT)) { visibility = SEMI_TRANSPARENT; } else { visibility = TRANSPARENT; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt index 878c2deb43b2..d8e0cfe4fbf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor +import com.android.systemui.util.time.fakeSystemClock val Kosmos.notifChipsViewModel: NotifChipsViewModel by Kosmos.Fixture { @@ -29,5 +30,6 @@ val Kosmos.notifChipsViewModel: NotifChipsViewModel by applicationCoroutineScope, statusBarNotificationChipsInteractor, headsUpNotificationInteractor, + fakeSystemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt new file mode 100644 index 000000000000..59f5ecd2563f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 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.notification + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.content.applicationContext +import android.graphics.drawable.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.icon.IconPack +import com.android.systemui.statusbar.notification.promoted.setPromotedContent +import org.mockito.kotlin.mock + +fun Kosmos.setIconPackWithMockIconViews(entry: NotificationEntry) { + entry.icons = + IconPack.buildPack( + /* statusBarIcon = */ mock(), + /* statusBarChipIcon = */ mock(), + /* shelfIcon = */ mock(), + /* aodIcon = */ mock(), + /* source = */ null, + ) +} + +fun Kosmos.buildOngoingCallEntry( + promoted: Boolean = false, + block: NotificationEntryBuilder.() -> Unit = {}, +): NotificationEntry = + buildNotificationEntry( + tag = "call", + promoted = promoted, + style = makeOngoingCallStyle(), + block = block, + ) + +fun Kosmos.buildPromotedOngoingEntry( + block: NotificationEntryBuilder.() -> Unit = {} +): NotificationEntry = + buildNotificationEntry(tag = "ron", promoted = true, style = null, block = block) + +fun Kosmos.buildNotificationEntry( + tag: String? = null, + promoted: Boolean = false, + style: Notification.Style? = null, + block: NotificationEntryBuilder.() -> Unit = {}, +): NotificationEntry = + NotificationEntryBuilder() + .apply { + setTag(tag) + setFlag(applicationContext, Notification.FLAG_PROMOTED_ONGOING, promoted) + modifyNotification(applicationContext) + .setSmallIcon(Icon.createWithContentUri("content://null")) + .setStyle(style) + } + .apply(block) + .build() + .also { + setIconPackWithMockIconViews(it) + if (promoted) setPromotedContent(it) + } + +private fun Kosmos.makeOngoingCallStyle(): Notification.CallStyle { + val pendingIntent = + PendingIntent.getBroadcast( + applicationContext, + 0, + Intent("action"), + PendingIntent.FLAG_IMMUTABLE, + ) + val person = Person.Builder().setName("person").build() + return Notification.CallStyle.forOngoingCall(person, pendingIntent) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt index a48b27015c02..fa3702cea5ee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock -var Kosmos.notifPipeline by Kosmos.Fixture { mock<NotifPipeline>() } +var Kosmos.notifPipeline by Kosmos.Fixture { mockNotifPipeline } +var Kosmos.mockNotifPipeline by Kosmos.Fixture { mock<NotifPipeline>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt index 63521de096c9..e55cd0dc16f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.promoted +import android.app.Notification import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform var Kosmos.promotedNotificationContentExtractor by @@ -28,3 +31,14 @@ var Kosmos.promotedNotificationContentExtractor by promotedNotificationLogger, ) } + +fun Kosmos.setPromotedContent(entry: NotificationEntry) { + val extractedContent = + promotedNotificationContentExtractor.extractContent( + entry, + Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification), + RowImageInflater.newInstance(null).useForContentModel(), + ) + entry.promotedNotificationContentModel = + requireNotNull(extractedContent) { "extractContent returned null" } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt index df1c82278bc2..fcd484353011 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt @@ -18,12 +18,11 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.aodPromotedNotificationInteractor by Kosmos.Fixture { AODPromotedNotificationInteractor( - activeNotificationsInteractor = activeNotificationsInteractor, + promotedNotificationsInteractor = promotedNotificationsInteractor, dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt new file mode 100644 index 000000000000..093ec10e2642 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 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.notification.promoted.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor + +val Kosmos.promotedNotificationsInteractor by + Kosmos.Fixture { + PromotedNotificationsInteractor( + activeNotificationsInteractor = activeNotificationsInteractor, + callChipInteractor = callChipInteractor, + notifChipsInteractor = statusBarNotificationChipsInteractor, + backgroundDispatcher = testDispatcher, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index e445a73b06d0..2543ca95eb3b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -41,6 +41,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag import com.android.systemui.media.dialog.MediaOutputDialogManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.shared.system.DevicePolicyManagerWrapper import com.android.systemui.shared.system.PackageManagerWrapper @@ -346,10 +347,14 @@ class ExpandableNotificationRowBuilder( // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be // set, but we do not want to override an existing value that is needed by a specific test. + val userTracker = Mockito.mock(UserTracker::class.java, STUB_ONLY) + whenever(userTracker.userHandle).thenReturn(context.user) + val rowInflaterTask = RowInflaterTask( mFakeSystemClock, Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY), + userTracker ) val row = rowInflaterTask.inflateSynchronously(context, null, entry) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index bc1363ac3d5c..970b87cd368a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -33,7 +33,7 @@ import java.util.Optional val Kosmos.notificationListViewBinder by Fixture { NotificationListViewBinder( - backgroundDispatcher = testDispatcher, + inflationDispatcher = testDispatcher, hiderTracker = displaySwitchNotificationsHiderTracker, configuration = configurationState, falsingManager = falsingManager, diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 0aa7227ac7e6..d4bb1d52c111 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2614,7 +2614,8 @@ public final class DisplayManagerService extends SystemService { // Blank or unblank the display immediately to match the state requested // by the display power controller (if known). DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); - if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 + || android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display == null) { return null; @@ -5574,7 +5575,9 @@ public final class DisplayManagerService extends SystemService { final DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked( id).getPrimaryDisplayDeviceLocked(); final int flags = displayDevice.getDisplayDeviceInfoLocked().flags; - if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 + || android.companion.virtualdevice.flags.Flags + .correctVirtualDisplayPowerState()) { final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(id); if (displayPowerController != null) { diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 4779b690adfb..e7939bb50ece 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -371,7 +371,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mCallback = callback; mProjection = projection; mMediaProjectionCallback = mediaProjectionCallback; - mDisplayState = Display.STATE_ON; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + // The display's power state depends on the power state of the state of its + // display / power group, which we don't know here. Initializing to UNKNOWN allows + // the first call to requestDisplayStateLocked() to set the correct state. + // This also triggers VirtualDisplay.Callback to tell the owner the initial state. + mDisplayState = Display.STATE_UNKNOWN; + } else { + mDisplayState = Display.STATE_ON; + } mPendingChanges |= PENDING_SURFACE_CHANGE; mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror(); mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled(); @@ -564,14 +572,23 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mInfo.yDpi = mDensityDpi; mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame mInfo.flags = 0; - if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE - | DisplayDeviceInfo.FLAG_NEVER_BLANK; - } - if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { - mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } } else { - mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE + | DisplayDeviceInfo.FLAG_NEVER_BLANK; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { + mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; + } else { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } } if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) { mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 70044bd813ce..508bc2f811e0 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -365,7 +365,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return mCurrentImeUserId; } - /** + /** * Figures out the target IME user ID associated with the given {@code displayId}. * * @param displayId the display ID to be queried about diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index f7a4d3d9132c..889df512dd60 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -157,6 +157,12 @@ public class ZenModeHelper { static final int RULE_LIMIT_PER_PACKAGE = 100; private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30); + /** + * Amount of time since last activation after which implicit rules that have never been + * customized by the user are automatically cleaned up. + */ + private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30); + private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000; /** @@ -534,7 +540,7 @@ public class ZenModeHelper { ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); if (sleepingRule != null && !sleepingRule.enabled - && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) { + && !sleepingRule.isUserModified()) { config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); } } @@ -864,7 +870,7 @@ public class ZenModeHelper { // We don't try to preserve system-owned rules because their conditionIds (used as // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot // delete a system-owned rule. - if (origin == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() + if (origin == ORIGIN_APP && ruleToRemove.isUserModified() && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) { String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove); if (deletedKey != null) { @@ -1282,7 +1288,7 @@ public class ZenModeHelper { // * the request comes from an origin that can always update values, like the user, or // * the rule has not yet been user modified, and thus can be updated by the app. boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin) - || rule.canBeUpdatedByApp(); + || !rule.isUserModified(); // For all other values, if updates are not allowed, we discard the update. if (!updateValues) { @@ -1914,6 +1920,7 @@ public class ZenModeHelper { * <ul> * <li>Rule instances whose owner is not installed. * <li>Deleted rules that were deleted more than 30 days ago. + * <li>Implicit rules that haven't been used in 30 days (and have not been customized). * </ul> */ private void cleanUpZenRules() { @@ -1932,6 +1939,10 @@ public class ZenModeHelper { } } + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + deleteUnusedImplicitRules(newConfig.automaticRules); + } + if (!newConfig.equals(mConfig)) { setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "cleanUpZenRules", Process.SYSTEM_UID); @@ -1957,6 +1968,29 @@ public class ZenModeHelper { } } + private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) { + if (ruleList == null) { + return; + } + Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR); + + for (int i = ruleList.size() - 1; i >= 0; i--) { + ZenRule rule = ruleList.valueAt(i); + if (isImplicitRuleId(rule.id) && !rule.isUserModified()) { + if (rule.lastActivation == null) { + // This rule existed before we started tracking activation time. It *might* be + // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR) + // before being removed if truly unused. + rule.lastActivation = mClock.instant(); + } + + if (rule.lastActivation.isBefore(deleteIfUnusedSince)) { + ruleList.removeAt(i); + } + } + } + } + /** * @return a copy of the zen mode configuration */ @@ -2091,6 +2125,20 @@ public class ZenModeHelper { } } + // Update last activation for rules that are being activated. + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + Instant now = mClock.instant(); + if (!mConfig.isManualActive() && config.isManualActive()) { + config.manualRule.lastActivation = now; + } + for (ZenRule rule : config.automaticRules.values()) { + ZenRule previousRule = mConfig.automaticRules.get(rule.id); + if (rule.isActive() && (previousRule == null || !previousRule.isActive())) { + rule.lastActivation = now; + } + } + } + mConfig = config; dispatchOnConfigChanged(); updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 4c1544f14667..67efb9e76692 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -488,33 +488,33 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = null; rule.zenDeviceEffects = null; - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.userModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_policyModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = new ZenPolicy(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenPolicyUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenDeviceEffectsUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test @@ -563,6 +563,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(456); + } } config.automaticRules.put(rule.id, rule); @@ -600,6 +603,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, ruleActual.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, ruleActual.lastActivation); + } } if (Flags.backupRestoreLogging()) { verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2); @@ -633,6 +639,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(789); + } } Parcel parcel = Parcel.obtain(); @@ -664,6 +673,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, parceled.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, parceled.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, parceled.lastActivation); + } } assertEquals(rule, parceled); @@ -746,6 +758,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_APP; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(123); + } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -781,6 +796,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, fromXml.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, fromXml.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, fromXml.lastActivation); + } } } @@ -908,7 +926,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; assertThat(rule.userModifiedFields).isEqualTo(1); - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeRuleXml(rule, baos); @@ -916,7 +934,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule fromXml = readRuleXml(bais); assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields); - assertThat(fromXml.canBeUpdatedByApp()).isFalse(); + assertThat(fromXml.isUserModified()).isTrue(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 8a5f80cb3e49..6d0bf8b322fd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -475,7 +475,8 @@ public class ZenModeDiffTest extends UiServiceTestCase { // "Metadata" fields are never compared. Set<String> exemptFields = new LinkedHashSet<>( Set.of("userModifiedFields", "zenPolicyUserModifiedFields", - "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin")); + "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin", + "lastActivation")); // Flagged fields are only compared if their flag is on. if (Flags.modesUi()) { exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete. diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 4d2f105e27b3..0ab11e0cbe3d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -23,6 +23,7 @@ import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING; +import static android.app.Flags.FLAG_MODES_CLEANUP_IMPLICIT; import static android.app.Flags.FLAG_MODES_MULTIUSER; import static android.app.Flags.FLAG_MODES_UI; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; @@ -124,7 +125,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import static java.time.temporal.ChronoUnit.DAYS; + import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AlarmManager; @@ -219,7 +223,6 @@ import java.io.Reader; import java.io.StringWriter; import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; import java.util.LinkedList; @@ -2233,8 +2236,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id, implicitRuleBeforeModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -2271,8 +2273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { implicitRuleWithModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -4611,7 +4612,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isTrue(); + assertThat(storedRule.isUserModified()).isFalse(); } @Test @@ -4719,7 +4720,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { STATE_DISALLOW); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS @@ -4761,7 +4762,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( ZenDeviceEffects.FIELD_GRAYSCALE); } @@ -5713,8 +5714,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Start with deleted rules from 2 different packages. Instant now = Instant.ofEpochMilli(1701796461000L); - ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now); - ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now); + ZenRule pkg1Rule = newDeletedZenRule("1", "pkg1", now.minus(1, DAYS), now); + ZenRule pkg2Rule = newDeletedZenRule("2", "pkg2", now.minus(2, DAYS), now); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); @@ -5832,9 +5833,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testRuleCleanup() throws Exception { Instant now = Instant.ofEpochMilli(1701796461000L); - Instant yesterday = now.minus(1, ChronoUnit.DAYS); - Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS); - Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); mTestClock.setNowMillis(now.toEpochMilli()); when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt())) @@ -5847,24 +5848,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { config.user = 42; mZenModeHelper.mConfigs.put(42, config); // okay rules (not deleted, package exists, with a range of creation dates). - config.automaticRules.put("ar1", newZenRule("good_pkg", now, null)); - config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null)); - config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null)); + config.automaticRules.put("ar1", newZenRule("ar1", "good_pkg", now)); + config.automaticRules.put("ar2", newZenRule("ar2", "good_pkg", yesterday)); + config.automaticRules.put("ar3", newZenRule("ar3", "good_pkg", twoMonthsAgo)); // newish rules for a missing package - config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null)); + config.automaticRules.put("ar4", newZenRule("ar4", "bad_pkg", yesterday)); // oldish rules belonging to a missing package - config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null)); + config.automaticRules.put("ar5", newZenRule("ar5", "bad_pkg", aWeekAgo)); // rules deleted recently - config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday)); - config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo)); + config.deletedRules.put("del1", + newDeletedZenRule("del1", "good_pkg", twoMonthsAgo, yesterday)); + config.deletedRules.put("del2", + newDeletedZenRule("del2", "good_pkg", twoMonthsAgo, aWeekAgo)); // rules deleted a long time ago - config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del3", + newDeletedZenRule("del3", "good_pkg", twoMonthsAgo, twoMonthsAgo)); // rules for a missing package, created recently and deleted recently - config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now)); + config.deletedRules.put("del4", newDeletedZenRule("del4", "bad_pkg", yesterday, now)); // rules for a missing package, created a long time ago and deleted recently - config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now)); + config.deletedRules.put("del5", newDeletedZenRule("del5", "bad_pkg", twoMonthsAgo, now)); // rules for a missing package, created a long time ago and deleted a long time ago - config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del6", + newDeletedZenRule("del6", "bad_pkg", twoMonthsAgo, twoMonthsAgo)); mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. @@ -5874,14 +5879,115 @@ public class ZenModeHelperTest extends UiServiceTestCase { .containsExactly("del1", "del2", "del4"); } - private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) { + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_removesNotRecentlyUsedNotModifiedImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing a bunch of implicit rules + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // used recently + ZenRule usedRecently1 = newImplicitZenRule("pkg1", aYearAgo, yesterday); + ZenRule usedRecently2 = newImplicitZenRule("pkg2", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently1.id, usedRecently1); + config.automaticRules.put(usedRecently2.id, usedRecently2); + // not used in a long time + ZenRule longUnused = newImplicitZenRule("pkg3", aYearAgo, twoMonthsAgo); + config.automaticRules.put(longUnused.id, longUnused); + // created a long time ago, before lastActivation tracking + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", twoMonthsAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, before lastActivation tracking + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + // not used in a long time, but was customized by user + ZenRule longUnusedButCustomized = newImplicitZenRule("pkg6", aYearAgo, twoMonthsAgo); + longUnusedButCustomized.zenPolicyUserModifiedFields = ZenPolicy.FIELD_CONVERSATIONS; + config.automaticRules.put(longUnusedButCustomized.id, longUnusedButCustomized); + // created a long time ago, before lastActivation tracking, and was customized by user + ZenRule oldAndLastUsageUnknownAndCustomized = newImplicitZenRule("pkg7", twoMonthsAgo, + null); + oldAndLastUsageUnknownAndCustomized.userModifiedFields = AutomaticZenRule.FIELD_ICON; + config.automaticRules.put(oldAndLastUsageUnknownAndCustomized.id, + oldAndLastUsageUnknownAndCustomized); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // The recently used OR modified OR last-used-unknown rules stay. + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_METADATA) + .containsExactly(usedRecently1, usedRecently2, oldAndLastUsageUnknown, + newAndLastUsageUnknown, longUnusedButCustomized, + oldAndLastUsageUnknownAndCustomized); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_assignsLastActivationToImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant aWeekAgo = now.minus(7, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing implicit rules. + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // with last activation known + ZenRule usedRecently = newImplicitZenRule("pkg1", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently.id, usedRecently); + // created a long time ago, with last activation unknown + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", aYearAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, with last activation unknown + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // All rules stayed. + usedRecently = getZenRule(usedRecently.id); + oldAndLastUsageUnknown = getZenRule(oldAndLastUsageUnknown.id); + newAndLastUsageUnknown = getZenRule(newAndLastUsageUnknown.id); + + // The rules with an unknown last usage have been assigned a placeholder one. + assertThat(usedRecently.lastActivation).isEqualTo(aWeekAgo); + assertThat(oldAndLastUsageUnknown.lastActivation).isEqualTo(now); + assertThat(newAndLastUsageUnknown.lastActivation).isEqualTo(now); + } + + private static ZenRule newDeletedZenRule(String id, String pkg, Instant createdAt, + @NonNull Instant deletedAt) { + ZenRule rule = newZenRule(id, pkg, createdAt); + rule.deletionInstant = deletedAt; + return rule; + } + + private static ZenRule newImplicitZenRule(String pkg, @NonNull Instant createdAt, + @Nullable Instant lastActivatedAt) { + ZenRule implicitRule = newZenRule(implicitRuleId(pkg), pkg, createdAt); + implicitRule.lastActivation = lastActivatedAt; + return implicitRule; + } + + private static ZenRule newZenRule(String id, String pkg, Instant createdAt) { ZenRule rule = new ZenRule(); + rule.id = id; rule.pkg = pkg; rule.creationTime = createdAt.toEpochMilli(); rule.enabled = true; - rule.deletionInstant = deletedAt; + rule.deletionInstant = null; // Plus stuff so that isValidAutomaticRule() passes - rule.name = "A rule from " + pkg + " created on " + createdAt; + rule.name = "Rule " + id; rule.conditionId = Uri.parse(rule.name); return rule; } @@ -5919,11 +6025,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() { // Assume existence of a system-owned rule that is currently ACTIVE. - ZenRule systemRule = newZenRule("android", Instant.now(), null); + ZenRule systemRule = newZenRule("systemRule", "android", Instant.now()); systemRule.zenMode = ZEN_MODE_ALARMS; systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("systemRule", systemRule); + config.automaticRules.put(systemRule.id, systemRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -5935,11 +6041,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setAutomaticZenRuleState_idForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -5955,11 +6061,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -7255,6 +7361,125 @@ public class ZenModeHelperTest extends UiServiceTestCase { "config: setAzrStateFromCps: cond/cond (ORIGIN_APP) from uid " + CUSTOM_PKG_UID); } + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_updatesLastActivation() { + String ruleOne = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String ruleTwo = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("unrelated", Uri.parse("other.condition")) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isNull(); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_FALSE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant secondActivation = Instant.ofEpochMilli(500); + mTestClock.setNow(secondActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(secondActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setManualZenMode_updatesLastActivation() { + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isNull(); + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_ALARMS, null, + ORIGIN_USER_IN_SYSTEMUI, "reason", "systemui", SYSTEM_UID); + + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void applyGlobalZenModeAsImplicitZenRule_updatesLastActivation() { + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_ALARMS); + + ZenRule implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME)); + assertThat(implicitRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_notChangingActiveState_doesNotUpdateLastActivation() { + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + // Manual activation comes first + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + + // Now the app says the rule should be active (assume it's on a schedule, and the app + // doesn't listen to broadcasts so it doesn't know an override was present). This doesn't + // change the activation state. + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void addOrUpdateRule_doesNotUpdateLastActivation() { + AutomaticZenRule azr = new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, + new AutomaticZenRule.Builder(azr).setName("New name").build(), ORIGIN_APP, "reason", + CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); @@ -7272,22 +7497,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = - Correspondence.transforming(zr -> { - Parcel p = Parcel.obtain(); - try { - zr.writeToParcel(p, 0); - p.setDataPosition(0); - ZenRule copy = new ZenRule(p); - copy.creationTime = 0; - copy.userModifiedFields = 0; - copy.zenPolicyUserModifiedFields = 0; - copy.zenDeviceEffectsUserModifiedFields = 0; - return copy; - } finally { - p.recycle(); - } - }, - "Ignoring timestamp and userModifiedFields"); + Correspondence.transforming( + ZenModeHelperTest::cloneWithoutMetadata, + ZenModeHelperTest::cloneWithoutMetadata, + "Ignoring timestamps and userModifiedFields"); + + private static ZenRule cloneWithoutMetadata(ZenRule rule) { + Parcel p = Parcel.obtain(); + try { + rule.writeToParcel(p, 0); + p.setDataPosition(0); + ZenRule copy = new ZenRule(p); + copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; + copy.lastActivation = null; + return copy; + } finally { + p.recycle(); + } + } private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { @@ -7693,6 +7923,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { return mNowMillis; } + private void setNow(Instant instant) { + mNowMillis = instant.toEpochMilli(); + } + private void setNowMillis(long millis) { mNowMillis = millis; } diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 83d22d923c78..4d379e45a81a 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -18,18 +18,24 @@ package android.os.test; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.os.TestLooperManager; import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Queue; import java.util.concurrent.Executor; /** @@ -44,7 +50,9 @@ import java.util.concurrent.Executor; * The Robolectric class also allows advancing time. */ public class TestLooper { - protected final Looper mLooper; + private final Looper mLooper; + private final TestLooperManager mTestLooperManager; + private final Clock mClock; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; @@ -54,24 +62,46 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private final Clock mClock; - private AutoDispatchThread mAutoDispatchThread; + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + Method[] methods = TestLooperManager.class.getMethods(); + for (Method method : methods) { + if (method.getName().equals("peekWhen")) { + return true; + } + } + return false; + // TODO(shayba): delete the above, uncomment the below. + // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. + // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + static { try { LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE); LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + MESSAGE_MARK_IN_USE_METHOD = null; + } else { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); + MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + } } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -106,6 +136,13 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + if (isAtLeastBaklava()) { + mTestLooperManager = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); + } else { + mTestLooperManager = null; + } + mClock = clock; } @@ -117,19 +154,61 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedList() { + private Message getMessageLinkedListLegacy() { try { MessageQueue queue = mLooper.getQueue(); return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); } catch (IllegalAccessException e) { throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); + e); } } public void moveTimeForward(long milliSeconds) { + if (isAtLeastBaklava()) { + moveTimeForwardBaklava(milliSeconds); + } else { + moveTimeForwardLegacy(milliSeconds); + } + } + + private void moveTimeForwardBaklava(long milliSeconds) { + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mTestLooperManager.poll(); + if (message == null) { + break; + } + messages.add(message); + } + + // Repost all Messages back to the queue with a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; + } + + // Ugly trick to reset the Message's "in use" flag. + // This is needed because the Message cannot be re-enqueued if it's + // marked in use. + message.copyFrom(message); + + // Adjust the Message's delivery time. + long newWhen = message.getWhen() - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + + // Send the Message back to its Handler to be re-enqueued. + message.getTarget().sendMessageAtTime(message, newWhen); + } + } + + private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -147,12 +226,12 @@ public class TestLooper { return mClock.uptimeMillis(); } - private Message messageQueueNext() { + private Message messageQueueNextLegacy() { try { long now = currentTime(); Message prevMsg = null; - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); if (msg != null && msg.getTarget() == null) { // Stalled by a barrier. Find the next asynchronous message in // the queue. @@ -185,18 +264,46 @@ public class TestLooper { /** * @return true if there are pending messages in the message queue */ - public synchronized boolean isIdle() { - Message messageList = getMessageLinkedList(); + public boolean isIdle() { + if (isAtLeastBaklava()) { + return isIdleBaklava(); + } else { + return isIdleLegacy(); + } + } + + private boolean isIdleBaklava() { + Long when = mTestLooperManager.peekWhen(); + return when != null && currentTime() >= when; + } + private synchronized boolean isIdleLegacy() { + Message messageList = getMessageLinkedListLegacy(); return messageList != null && currentTime() >= messageList.getWhen(); } /** * @return the next message in the Looper's message queue or null if there is none */ - public synchronized Message nextMessage() { + public Message nextMessage() { + if (isAtLeastBaklava()) { + return nextMessageBaklava(); + } else { + return nextMessageLegacy(); + } + } + + private Message nextMessageBaklava() { + if (isIdle()) { + return mTestLooperManager.poll(); + } else { + return null; + } + } + + private synchronized Message nextMessageLegacy() { if (isIdle()) { - return messageQueueNext(); + return messageQueueNextLegacy(); } else { return null; } @@ -206,9 +313,26 @@ public class TestLooper { * Dispatch the next message in the queue * Asserts that there is a message in the queue */ - public synchronized void dispatchNext() { + public void dispatchNext() { + if (isAtLeastBaklava()) { + dispatchNextBaklava(); + } else { + dispatchNextLegacy(); + } + } + + private void dispatchNextBaklava() { + assertTrue(isIdle()); + Message msg = mTestLooperManager.poll(); + if (msg == null) { + return; + } + msg.getTarget().dispatchMessage(msg); + } + + private synchronized void dispatchNextLegacy() { assertTrue(isIdle()); - Message msg = messageQueueNext(); + Message msg = messageQueueNextLegacy(); if (msg == null) { return; } |