diff options
261 files changed, 7275 insertions, 1976 deletions
diff --git a/cmds/app_process/Android.bp b/cmds/app_process/Android.bp index 3c7609e1d8ed..a1575173ded6 100644 --- a/cmds/app_process/Android.bp +++ b/cmds/app_process/Android.bp @@ -56,7 +56,6 @@ cc_binary { "libsigchain", "libutils", - "libutilscallstack", // This is a list of libraries that need to be included in order to avoid // bad apps. This prevents a library from having a mismatch when resolving diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java index d3e62d5351f0..017d9563b9a8 100644 --- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java +++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java @@ -61,17 +61,18 @@ public class EvemuParser implements EventParser { mReader = in; } - private @Nullable String findNextLine() throws IOException { + private void findNextLine() throws IOException { String line = ""; while (line != null && line.length() == 0) { String unstrippedLine = mReader.readLine(); if (unstrippedLine == null) { mAtEndOfFile = true; - return null; + mNextLine = null; + return; } line = stripComments(unstrippedLine); } - return line; + mNextLine = line; } private static String stripComments(String line) { @@ -92,7 +93,7 @@ public class EvemuParser implements EventParser { */ public @Nullable String peekLine() throws IOException { if (mNextLine == null && !mAtEndOfFile) { - mNextLine = findNextLine(); + findNextLine(); } return mNextLine; } @@ -103,7 +104,10 @@ public class EvemuParser implements EventParser { mNextLine = null; } - public boolean isAtEndOfFile() { + public boolean isAtEndOfFile() throws IOException { + if (mNextLine == null && !mAtEndOfFile) { + findNextLine(); + } return mAtEndOfFile; } diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java index 5239fbc7e0a8..f18cab51fb4d 100644 --- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java +++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java @@ -216,6 +216,9 @@ public class EvemuParserTest { assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1); assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); + + // Now we should be at the end of the file. + assertThat(parser.getNextEvent()).isNull(); } @Test @@ -246,6 +249,8 @@ public class EvemuParserTest { assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000); assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0); + + assertThat(parser.getNextEvent()).isNull(); } @Test diff --git a/core/api/current.txt b/core/api/current.txt index 151a6738505c..6416e07b17f3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -44050,6 +44050,7 @@ package android.telecom { field public static final String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED"; field public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD"; field public static final String EVENT_CALL_REMOTELY_UNHELD = "android.telecom.event.CALL_REMOTELY_UNHELD"; + field @FlaggedApi("com.android.server.telecom.flags.call_sequencing_call_resume_failed") public static final String EVENT_CALL_RESUME_FAILED = "android.telecom.event.CALL_RESUME_FAILED"; field public static final String EVENT_CALL_SWITCH_FAILED = "android.telecom.event.CALL_SWITCH_FAILED"; field public static final String EVENT_MERGE_COMPLETE = "android.telecom.event.MERGE_COMPLETE"; field public static final String EVENT_MERGE_START = "android.telecom.event.MERGE_START"; @@ -56113,6 +56114,7 @@ package android.view { @FlaggedApi("android.xr.xr_manifest_entries") public final class XrWindowProperties { field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_ACTIVITY_START_MODE = "android.window.PROPERTY_XR_ACTIVITY_START_MODE"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED = "android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION = "android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED"; field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_HOME_SPACE = "XR_ACTIVITY_START_MODE_HOME_SPACE"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 42c60b0ba0da..b92df4cf7884 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -18977,10 +18977,8 @@ package android.view { public static class WindowManager.LayoutParams extends android.view.ViewGroup.LayoutParams implements android.os.Parcelable { method public final long getUserActivityTimeout(); - method @FlaggedApi("com.android.hardware.input.override_power_key_behavior_in_focused_window") @RequiresPermission(android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) public boolean isReceivePowerKeyDoublePressEnabled(); method public boolean isSystemApplicationOverlay(); method @FlaggedApi("android.companion.virtualdevice.flags.status_bar_and_insets") public void setInsetsParams(@NonNull java.util.List<android.view.WindowManager.InsetsParams>); - method @FlaggedApi("com.android.hardware.input.override_power_key_behavior_in_focused_window") @RequiresPermission(android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) public void setReceivePowerKeyDoublePressEnabled(boolean); method @RequiresPermission(android.Manifest.permission.SYSTEM_APPLICATION_OVERLAY) public void setSystemApplicationOverlay(boolean); method public final void setUserActivityTimeout(long); field @RequiresPermission(android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) public static final int SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 524288; // 0x80000 diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 4c3deada4d76..521b70b599f6 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6122,6 +6122,21 @@ public class Notification implements Parcelable return mColors; } + private void updateHeaderBackgroundColor(RemoteViews contentView, + StandardTemplateParams p) { + if (!Flags.uiRichOngoing()) { + return; + } + if (isBackgroundColorized(p)) { + contentView.setInt(R.id.notification_header, "setBackgroundColor", + getBackgroundColor(p)); + } else { + // Clear it! + contentView.setInt(R.id.notification_header, "setBackgroundResource", + 0); + } + } + private void updateBackgroundColor(RemoteViews contentView, StandardTemplateParams p) { if (isBackgroundColorized(p)) { @@ -6926,7 +6941,7 @@ public class Notification implements Parcelable * @hide */ public RemoteViews makeNotificationGroupHeader() { - return makeNotificationHeader(mParams.reset() + return makeNotificationHeader(mParams.reset().disallowColorization() .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER) .fillTextsFrom(this)); } @@ -6938,12 +6953,11 @@ public class Notification implements Parcelable * @param p the template params to inflate this with */ private RemoteViews makeNotificationHeader(StandardTemplateParams p) { - // Headers on their own are never colorized - p.disallowColorization(); RemoteViews header = new BuilderRemoteViews(mContext.getApplicationInfo(), getHeaderLayoutResource()); resetNotificationHeader(header); bindNotificationHeader(header, p); + updateHeaderBackgroundColor(header, p); if (Flags.notificationsRedesignTemplates() && (p.mViewType == StandardTemplateParams.VIEW_TYPE_MINIMIZED || p.mViewType == StandardTemplateParams.VIEW_TYPE_PUBLIC)) { @@ -7067,6 +7081,10 @@ public class Notification implements Parcelable savedBundle.getBoolean(EXTRA_SHOW_CHRONOMETER)); publicExtras.putBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, savedBundle.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN)); + if (mN.isPromotedOngoing()) { + publicExtras.putBoolean(EXTRA_COLORIZED, + savedBundle.getBoolean(EXTRA_COLORIZED)); + } String appName = savedBundle.getString(EXTRA_SUBSTITUTE_APP_NAME); if (appName != null) { publicExtras.putString(EXTRA_SUBSTITUTE_APP_NAME, appName); @@ -7079,6 +7097,9 @@ public class Notification implements Parcelable if (isLowPriority) { params.highlightExpander(false); } + if (!mN.isPromotedOngoing()) { + params.disallowColorization(); + } view = makeNotificationHeader(params); view.setBoolean(R.id.notification_header, "setExpandOnlyOnButton", true); mN.extras = savedBundle; @@ -7098,7 +7119,7 @@ public class Notification implements Parcelable * @hide */ public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) { - StandardTemplateParams p = mParams.reset() + StandardTemplateParams p = mParams.reset().disallowColorization() .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED) .highlightExpander(false) .fillTextsFrom(this); diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java index e3f67811757c..a085701b006a 100644 --- a/core/java/android/app/jank/JankTracker.java +++ b/core/java/android/app/jank/JankTracker.java @@ -143,6 +143,13 @@ public class JankTracker { * stats */ public void mergeAppJankStats(AppJankStats appJankStats) { + if (appJankStats.getUid() != mAppUid) { + if (DEBUG) { + Log.d(DEBUG_KEY, "Reported JankStats AppUID does not match AppUID of " + + "enclosing activity."); + } + return; + } getHandler().post(new Runnable() { @Override public void run() { diff --git a/core/java/android/content/theming/FieldColor.java b/core/java/android/content/theming/FieldColor.java new file mode 100644 index 000000000000..a06a54f362b5 --- /dev/null +++ b/core/java/android/content/theming/FieldColor.java @@ -0,0 +1,80 @@ +/* + * 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 android.content.theming; + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.graphics.Color; + +import androidx.annotation.Nullable; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColor extends ThemeSettingsField<Integer, String> { + private static final Pattern COLOR_PATTERN = Pattern.compile("[0-9a-fA-F]{6,8}"); + + public FieldColor( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @ColorInt + @Nullable + public Integer parse(String primitive) { + if (primitive == null) { + return null; + } + if (!COLOR_PATTERN.matcher(primitive).matches()) { + return null; + } + + try { + return Color.valueOf(Color.parseColor("#" + primitive)).toArgb(); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public String serialize(@ColorInt Integer value) { + return Integer.toHexString(value); + } + + @Override + public boolean validate(Integer value) { + return !value.equals(Color.TRANSPARENT); + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorBoth.java b/core/java/android/content/theming/FieldColorBoth.java new file mode 100644 index 000000000000..e4a9f7f716d8 --- /dev/null +++ b/core/java/android/content/theming/FieldColorBoth.java @@ -0,0 +1,70 @@ +/* + * 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 android.content.theming; + +import android.annotation.FlaggedApi; + +import androidx.annotation.Nullable; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorBoth extends ThemeSettingsField<Boolean, String> { + public FieldColorBoth( + String key, + BiConsumer<ThemeSettingsUpdater, Boolean> setter, + Function<ThemeSettings, Boolean> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @Nullable + public Boolean parse(String primitive) { + return switch (primitive) { + case "1" -> true; + case "0" -> false; + default -> null; + }; + } + + @Override + public String serialize(Boolean typedValue) { + if (typedValue) return "1"; + return "0"; + } + + @Override + public boolean validate(Boolean value) { + Objects.requireNonNull(value); + return true; + } + + @Override + public Class<Boolean> getFieldType() { + return Boolean.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorIndex.java b/core/java/android/content/theming/FieldColorIndex.java new file mode 100644 index 000000000000..683568a42318 --- /dev/null +++ b/core/java/android/content/theming/FieldColorIndex.java @@ -0,0 +1,64 @@ +/* + * 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 android.content.theming; + +import android.annotation.FlaggedApi; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorIndex extends ThemeSettingsField<Integer, String> { + public FieldColorIndex( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + public Integer parse(String primitive) { + try { + return Integer.parseInt(primitive); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String serialize(Integer typedValue) { + return typedValue.toString(); + } + + @Override + public boolean validate(Integer value) { + return value >= -1; + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorSource.java b/core/java/android/content/theming/FieldColorSource.java new file mode 100644 index 000000000000..1ff3aa64fda5 --- /dev/null +++ b/core/java/android/content/theming/FieldColorSource.java @@ -0,0 +1,76 @@ +/* + * 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 android.content.theming; + +import android.annotation.FlaggedApi; +import android.annotation.StringDef; + +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorSource extends ThemeSettingsField<String, String> { + public FieldColorSource( + String key, + BiConsumer<ThemeSettingsUpdater, String> setter, + Function<ThemeSettings, String> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @Nullable + @Type + public String parse(String primitive) { + return primitive; + } + + @Override + public String serialize(@Type String typedValue) { + return typedValue; + } + + @Override + public boolean validate(String value) { + return switch (value) { + case "preset", "home_wallpaper", "lock_wallpaper" -> true; + default -> false; + }; + } + + @Override + public Class<String> getFieldType() { + return String.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } + + + @StringDef({"preset", "home_wallpaper", "lock_wallpaper"}) + @Retention(RetentionPolicy.SOURCE) + @interface Type { + } +} diff --git a/core/java/android/content/theming/FieldThemeStyle.java b/core/java/android/content/theming/FieldThemeStyle.java new file mode 100644 index 000000000000..b433e5b96ec3 --- /dev/null +++ b/core/java/android/content/theming/FieldThemeStyle.java @@ -0,0 +1,76 @@ +/* + * 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 android.content.theming; + +import android.annotation.FlaggedApi; +import android.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldThemeStyle extends ThemeSettingsField<Integer, String> { + public FieldThemeStyle( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + private static final @ThemeStyle.Type List<Integer> sValidStyles = Arrays.asList( + ThemeStyle.EXPRESSIVE, + ThemeStyle.SPRITZ, + ThemeStyle.TONAL_SPOT, ThemeStyle.FRUIT_SALAD, ThemeStyle.RAINBOW, + ThemeStyle.VIBRANT, + ThemeStyle.MONOCHROMATIC); + + @Override + public String serialize(@ThemeStyle.Type Integer typedValue) { + return ThemeStyle.toString(typedValue); + } + + @Override + public boolean validate(Integer value) { + return sValidStyles.contains(value); + } + + @Override + @Nullable + @ThemeStyle.Type + public Integer parse(String primitive) { + try { + return ThemeStyle.valueOf(primitive); + } catch (Exception e) { + return null; + } + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/ThemeSettings.java b/core/java/android/content/theming/ThemeSettings.java new file mode 100644 index 000000000000..e94c1fef5382 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettings.java @@ -0,0 +1,200 @@ +/* + * 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 android.content.theming; + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** + * Represents the theme settings for the system. + * This class holds various properties related to theming, such as color indices, palettes, + * accent colors, color sources, theme styles, and color combinations. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public final class ThemeSettings implements Parcelable { + private final int mColorIndex; + private final int mSystemPalette; + private final int mAccentColor; + @NonNull + private final String mColorSource; + private final int mThemeStyle; + private final boolean mColorBoth; + + /** + * Constructs a new ThemeSettings object. + * + * @param colorIndex The color index. + * @param systemPalette The system palette color. + * @param accentColor The accent color. + * @param colorSource The color source. + * @param themeStyle The theme style. + * @param colorBoth The color combination. + */ + + public ThemeSettings(int colorIndex, @ColorInt int systemPalette, + @ColorInt int accentColor, @NonNull String colorSource, int themeStyle, + boolean colorBoth) { + + this.mAccentColor = accentColor; + this.mColorBoth = colorBoth; + this.mColorIndex = colorIndex; + this.mColorSource = colorSource; + this.mSystemPalette = systemPalette; + this.mThemeStyle = themeStyle; + } + + /** + * Constructs a ThemeSettings object from a Parcel. + * + * @param in The Parcel to read from. + */ + ThemeSettings(Parcel in) { + this.mAccentColor = in.readInt(); + this.mColorBoth = in.readBoolean(); + this.mColorIndex = in.readInt(); + this.mColorSource = Objects.requireNonNullElse(in.readString8(), "s"); + this.mSystemPalette = in.readInt(); + this.mThemeStyle = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAccentColor); + dest.writeBoolean(mColorBoth); + dest.writeInt(mColorIndex); + dest.writeString8(mColorSource); + dest.writeInt(mSystemPalette); + dest.writeInt(mThemeStyle); + } + + /** + * Gets the color index. + * + * @return The color index. + */ + public Integer colorIndex() { + return mColorIndex; + } + + /** + * Gets the system palette color. + * + * @return The system palette color. + */ + @ColorInt + public Integer systemPalette() { + return mSystemPalette; + } + + /** + * Gets the accent color. + * + * @return The accent color. + */ + @ColorInt + public Integer accentColor() { + return mAccentColor; + } + + /** + * Gets the color source. + * + * @return The color source. + */ + @FieldColorSource.Type + public String colorSource() { + return mColorSource; + } + + /** + * Gets the theme style. + * + * @return The theme style. + */ + @ThemeStyle.Type + public Integer themeStyle() { + return mThemeStyle; + } + + /** + * Gets the color combination. + * + * @return The color combination. + */ + public Boolean colorBoth() { + return mColorBoth; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + return obj instanceof ThemeSettings other + && mColorIndex == other.mColorIndex + && mSystemPalette == other.mSystemPalette + && mAccentColor == other.mAccentColor + && mColorSource.equals(other.mColorSource) + && mThemeStyle == other.mThemeStyle + && mColorBoth == other.mColorBoth; + } + + @Override + public int hashCode() { + return Objects.hash(mColorIndex, mSystemPalette, mAccentColor, mColorSource, mThemeStyle, + mColorBoth); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Creator for Parcelable interface. + */ + public static final Creator<ThemeSettings> CREATOR = new Creator<>() { + @Override + public ThemeSettings createFromParcel(Parcel in) { + return new ThemeSettings(in); + } + + @Override + public ThemeSettings[] newArray(int size) { + return new ThemeSettings[size]; + } + }; + + /** + * Creates a new {@link ThemeSettingsUpdater} instance for updating the {@link ThemeSettings} + * through the API. + * + * @return A new {@link ThemeSettingsUpdater} instance. + */ + public static ThemeSettingsUpdater updater() { + return new ThemeSettingsUpdater(); + } +} diff --git a/core/java/android/content/theming/ThemeSettingsField.java b/core/java/android/content/theming/ThemeSettingsField.java new file mode 100644 index 000000000000..1696df4ad0f6 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettingsField.java @@ -0,0 +1,287 @@ +/* + * 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 android.content.theming; + + +import android.annotation.FlaggedApi; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.Preconditions; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Represents a field within {@link ThemeSettings}, providing methods for parsing, serializing, + * managing default values, and validating the field's value. + * <p> + * This class is designed to be extended by concrete classes that represent specific fields within + * {@link ThemeSettings}. Each subclass should define the following methods, where T is the type of + * the field's value and J is the type of the field's value stored in JSON: + * <ul> + * <li>{@link #parse(Object)} to parse a JSON representation into the field's value type.</li> + * <li>{@link #serialize(Object)} to serialize the field's value into a JSON representation.</li> + * <li>{@link #validate(Object)} to validate the field's value.</li> + * <li>{@link #getFieldType()} to return the type of the field's value.</li> + * <li>{@link #getJsonType()} to return the type of the field's value stored in JSON.</li> + * </ul> + * <p> + * The {@link #fromJSON(JSONObject, ThemeSettingsUpdater)} and + * {@link #toJSON(ThemeSettings, JSONObject)} + * methods handle the extraction and serialization of the field's value to and from JSON objects + * respectively. The {@link #fallbackParse(Object, Object)} method is used to parse a string + * representation of the field's value, falling back to a default value if parsing fails. + * + * @param <T> The type of the field's value. + * @param <J> The type of the JSON property. + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public abstract class ThemeSettingsField<T, J> { + private static final String TAG = ThemeSettingsField.class.getSimpleName(); + + private static final String KEY_PREFIX = "android.theme.customization."; + public static final String OVERLAY_CATEGORY_ACCENT_COLOR = KEY_PREFIX + "accent_color"; + public static final String OVERLAY_CATEGORY_SYSTEM_PALETTE = KEY_PREFIX + "system_palette"; + public static final String OVERLAY_CATEGORY_THEME_STYLE = KEY_PREFIX + "theme_style"; + public static final String OVERLAY_COLOR_SOURCE = KEY_PREFIX + "color_source"; + public static final String OVERLAY_COLOR_INDEX = KEY_PREFIX + "color_index"; + public static final String OVERLAY_COLOR_BOTH = KEY_PREFIX + "color_both"; + + + /** + * Returns an array of all available {@link ThemeSettingsField} instances. + * + * @param defaults The default {@link ThemeSettings} object to use for default values. + * @return An array of {@link ThemeSettingsField} instances. + */ + public static ThemeSettingsField<?, ?>[] getFields(ThemeSettings defaults) { + return new ThemeSettingsField[]{ + new FieldColorIndex( + OVERLAY_COLOR_INDEX, + ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, + defaults), + new FieldColor( + OVERLAY_CATEGORY_SYSTEM_PALETTE, + ThemeSettingsUpdater::systemPalette, + ThemeSettings::systemPalette, + defaults), + new FieldColor( + OVERLAY_CATEGORY_ACCENT_COLOR, + ThemeSettingsUpdater::accentColor, + ThemeSettings::accentColor, + defaults), + new FieldColorSource( + OVERLAY_COLOR_SOURCE, + ThemeSettingsUpdater::colorSource, + ThemeSettings::colorSource, + defaults), + new FieldThemeStyle( + OVERLAY_CATEGORY_THEME_STYLE, + ThemeSettingsUpdater::themeStyle, + ThemeSettings::themeStyle, + defaults), + new FieldColorBoth( + OVERLAY_COLOR_BOTH, + ThemeSettingsUpdater::colorBoth, + ThemeSettings::colorBoth, + defaults) + }; + } + + public final String key; + private final BiConsumer<ThemeSettingsUpdater, T> mSetter; + private final Function<ThemeSettings, T> mGetter; + private final ThemeSettings mDefaults; + + /** + * Creates a new {@link ThemeSettingsField}. + * + * @param key The key to identify the field in JSON objects. + * @param setter The setter to update the field's value in a {@link ThemeSettingsUpdater}. + * @param getter The getter to retrieve the field's value from a {@link ThemeSettings} + * object. + * @param defaults The default {@link ThemeSettings} object to provide default values. + */ + + public ThemeSettingsField( + String key, + BiConsumer<ThemeSettingsUpdater, T> setter, + Function<ThemeSettings, T> getter, + ThemeSettings defaults + ) { + this.key = key; + mSetter = setter; + mGetter = getter; + mDefaults = defaults; + } + + /** + * Attempts to parse a JSON primitive representation of the field's value. If parsing fails, it + * defaults to the field's default value. + * + * @param primitive The string representation to parse. + */ + private T fallbackParse(Object primitive, T fallbackValue) { + if (primitive == null) { + Log.w(TAG, "Error, field `" + key + "` was not found, defaulting to " + fallbackValue); + return fallbackValue; + } + + if (!getJsonType().isInstance(primitive)) { + Log.w(TAG, "Error, field `" + key + "` expected to be of type `" + + getJsonType().getSimpleName() + + "`, got `" + primitive.getClass().getSimpleName() + "`, defaulting to " + + fallbackValue); + return fallbackValue; + } + + // skips parsing if destination json type is already the same as field type + T parsedValue = getFieldType() == getJsonType() ? (T) primitive : parse((J) primitive); + + if (parsedValue == null) { + Log.w(TAG, "Error parsing JSON field `" + key + "` , defaulting to " + fallbackValue); + return fallbackValue; + } + + if (!validate(parsedValue)) { + Log.w(TAG, + "Error validating JSON field `" + key + "` , defaulting to " + fallbackValue); + return fallbackValue; + } + + if (parsedValue.getClass() != getFieldType()) { + Log.w(TAG, "Error: JSON field `" + key + "` expected to be of type `" + + getFieldType().getSimpleName() + + "`, defaulting to " + fallbackValue); + return fallbackValue; + } + + return parsedValue; + } + + + /** + * Extracts the field's value from a JSON object and sets it in a + * {@link ThemeSettingsUpdater}. + * + * @param source The JSON object containing the field's value. + */ + public void fromJSON(JSONObject source, ThemeSettingsUpdater updater) { + Object primitiveStr = source.opt(key); + T typedValue = fallbackParse(primitiveStr, getDefaultValue()); + mSetter.accept(updater, typedValue); + } + + /** + * Serializes the field's value from a {@link ThemeSettings} object into a JSON object. + * + * @param source The {@link ThemeSettings} object from which to retrieve the field's + * value. + * @param destination The JSON object to which the field's value will be added. + */ + public void toJSON(ThemeSettings source, JSONObject destination) { + T value = mGetter.apply(source); + Preconditions.checkState(value.getClass() == getFieldType()); + + J serialized; + if (validate(value)) { + serialized = serialize(value); + } else { + T fallbackValue = getDefaultValue(); + serialized = serialize(fallbackValue); + Log.w(TAG, "Invalid value `" + value + "` for key `" + key + "`, defaulting to '" + + fallbackValue); + } + + try { + destination.put(key, serialized); + } catch (JSONException e) { + Log.d(TAG, + "Error writing JSON primitive, skipping field " + key + ", " + e.getMessage()); + } + } + + + /** + * Returns the default value of the field. + * + * @return The default value. + */ + @VisibleForTesting + @NonNull + public T getDefaultValue() { + return mGetter.apply(mDefaults); + } + + /** + * Parses a string representation into the field's value type. + * + * @param primitive The string representation to parse. + * @return The parsed value, or null if parsing fails. + */ + @VisibleForTesting + @Nullable + public abstract T parse(J primitive); + + /** + * Serializes the field's value into a primitive type suitable for JSON. + * + * @param value The value to serialize. + * @return The serialized value. + */ + @VisibleForTesting + public abstract J serialize(T value); + + /** + * Validates the field's value. + * This method can be overridden to perform custom validation logic and MUST NOT validate for + * nullity. + * + * @param value The value to validate. + * @return {@code true} if the value is valid, {@code false} otherwise. + */ + @VisibleForTesting + public abstract boolean validate(T value); + + /** + * Returns the type of the field's value. + * + * @return The type of the field's value. + */ + @VisibleForTesting + public abstract Class<T> getFieldType(); + + /** + * Returns the type of the field's value stored in JSON. + * + * <p>This method is used to determine the expected type of the field's value when it is + * stored in a JSON object. + * + * @return The type of the field's value stored in JSON. + */ + @VisibleForTesting + public abstract Class<J> getJsonType(); +} diff --git a/core/java/android/content/theming/ThemeSettingsUpdater.java b/core/java/android/content/theming/ThemeSettingsUpdater.java new file mode 100644 index 000000000000..acd7d356db69 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettingsUpdater.java @@ -0,0 +1,244 @@ +/* + * 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 android.content.theming; + + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.VisibleForTesting; + +import java.util.Objects; + +/** + * Updater class for constructing {@link ThemeSettings} objects. + * This class provides a fluent interface for setting the various properties of the theme + * settings. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class ThemeSettingsUpdater implements Parcelable { + @ColorInt + private Integer mAccentColor; + private Boolean mColorBoth; + private Integer mColorIndex; + private String mColorSource; + @ColorInt + private Integer mSystemPalette; + private Integer mThemeStyle; + + ThemeSettingsUpdater(Integer colorIndex, @ColorInt Integer systemPalette, + @ColorInt Integer accentColor, @FieldColorSource.Type String colorSource, + @ThemeStyle.Type Integer themeStyle, Boolean colorBoth) { + this.mAccentColor = accentColor; + this.mColorBoth = colorBoth; + this.mColorIndex = colorIndex; + this.mColorSource = colorSource; + this.mSystemPalette = systemPalette; + this.mThemeStyle = themeStyle; + } + + ThemeSettingsUpdater() { + } + + // only reading basic JVM types for nullability + @SuppressLint("ParcelClassLoader") + protected ThemeSettingsUpdater(Parcel in) { + mAccentColor = (Integer) in.readValue(null); + mColorBoth = (Boolean) in.readValue(null); + mColorIndex = (Integer) in.readValue(null); + mColorSource = (String) in.readValue(null); + mSystemPalette = (Integer) in.readValue(null); + mThemeStyle = (Integer) in.readValue(null); + } + + // using read/writeValue for nullability support + @SuppressWarnings("AndroidFrameworkEfficientParcelable") + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(mAccentColor); + dest.writeValue(mColorBoth); + dest.writeValue(mColorIndex); + dest.writeValue(mColorSource); + dest.writeValue(mSystemPalette); + dest.writeValue(mThemeStyle); + } + + /** + * Sets the color index. + * + * @param colorIndex The color index to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorIndex(int colorIndex) { + this.mColorIndex = colorIndex; + return this; + } + + /** + * Returns the color index. + * + * @return The color index. + */ + @VisibleForTesting + public Integer getColorIndex() { + return mColorIndex; + } + + /** + * Sets the system palette color. + * + * @param systemPalette The system palette color to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater systemPalette(@ColorInt int systemPalette) { + this.mSystemPalette = systemPalette; + return this; + } + + /** + * Returns the system palette color. + * + * @return The system palette color. + */ + @VisibleForTesting + public Integer getSystemPalette() { + return mSystemPalette; + } + + /** + * Sets the accent color. + * + * @param accentColor The accent color to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater accentColor(@ColorInt int accentColor) { + this.mAccentColor = accentColor; + return this; + } + + /** + * Returns the accent color. + * + * @return The accent color. + */ + @VisibleForTesting + public Integer getAccentColor() { + return mAccentColor; + } + + /** + * Sets the color source. + * + * @param colorSource The color source to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorSource(@NonNull @FieldColorSource.Type String colorSource) { + this.mColorSource = colorSource; + return this; + } + + /** + * Returns the theme style. + * + * @return The theme style. + */ + @VisibleForTesting + public Integer getThemeStyle() { + return mThemeStyle; + } + + /** + * Sets the theme style. + * + * @param themeStyle The theme style to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater themeStyle(@ThemeStyle.Type int themeStyle) { + this.mThemeStyle = themeStyle; + return this; + } + + /** + * Returns the color source. + * + * @return The color source. + */ + @VisibleForTesting + public String getColorSource() { + return mColorSource; + } + + /** + * Sets the color combination. + * + * @param colorBoth The color combination to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorBoth(boolean colorBoth) { + this.mColorBoth = colorBoth; + return this; + } + + /** + * Returns the color combination. + * + * @return The color combination. + */ + @VisibleForTesting + public Boolean getColorBoth() { + return mColorBoth; + } + + /** + * Constructs a new {@link ThemeSettings} object with the current builder settings. + * + * @return A new {@link ThemeSettings} object. + */ + public ThemeSettings toThemeSettings(@NonNull ThemeSettings defaults) { + return new ThemeSettings( + Objects.requireNonNullElse(mColorIndex, defaults.colorIndex()), + Objects.requireNonNullElse(mSystemPalette, defaults.systemPalette()), + Objects.requireNonNullElse(mAccentColor, defaults.accentColor()), + Objects.requireNonNullElse(mColorSource, defaults.colorSource()), + Objects.requireNonNullElse(mThemeStyle, defaults.themeStyle()), + Objects.requireNonNullElse(mColorBoth, defaults.colorBoth())); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ThemeSettingsUpdater> CREATOR = + new Creator<>() { + @Override + public ThemeSettingsUpdater createFromParcel(Parcel in) { + return new ThemeSettingsUpdater(in); + } + + @Override + public ThemeSettingsUpdater[] newArray(int size) { + return new ThemeSettingsUpdater[size]; + } + }; +} diff --git a/core/java/android/content/theming/ThemeStyle.java b/core/java/android/content/theming/ThemeStyle.java new file mode 100644 index 000000000000..607896405020 --- /dev/null +++ b/core/java/android/content/theming/ThemeStyle.java @@ -0,0 +1,180 @@ +/* + * 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 android.content.theming; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class defining the different styles available for theming. + * This class replaces the previous enum implementation for improved performance and compatibility. + * + * @hide + */ +public final class ThemeStyle { + + private ThemeStyle() { + } + + /** + * @hide + */ + @IntDef({ + SPRITZ, + TONAL_SPOT, + VIBRANT, + EXPRESSIVE, + RAINBOW, + FRUIT_SALAD, + CONTENT, + MONOCHROMATIC, + CLOCK, + CLOCK_VIBRANT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + } + + /** + * Represents the SPRITZ style. + */ + public static final int SPRITZ = 0; + /** + * Represents the TONAL_SPOT style. + */ + public static final int TONAL_SPOT = 1; + /** + * Represents the VIBRANT style. + */ + public static final int VIBRANT = 2; + /** + * Represents the EXPRESSIVE style. + */ + public static final int EXPRESSIVE = 3; + /** + * Represents the RAINBOW style. + */ + public static final int RAINBOW = 4; + /** + * Represents the FRUIT_SALAD style. + */ + public static final int FRUIT_SALAD = 5; + /** + * Represents the CONTENT style. + */ + public static final int CONTENT = 6; + /** + * Represents the MONOCHROMATIC style. + */ + public static final int MONOCHROMATIC = 7; + /** + * Represents the CLOCK style. + */ + public static final int CLOCK = 8; + /** + * Represents the CLOCK_VIBRANT style. + */ + public static final int CLOCK_VIBRANT = 9; + + + /** + * Returns the string representation of the given style. + * + * @param style The style value. + * @return The string representation of the style. + * @throws IllegalArgumentException if the style value is invalid. + */ + @NonNull + public static String toString(@Nullable @Type Integer style) { + // Throw an exception if style is null + if (style == null) { + throw new IllegalArgumentException("Invalid style value: null"); + } + + return switch (style) { + case SPRITZ -> "SPRITZ"; + case TONAL_SPOT -> "TONAL_SPOT"; + case VIBRANT -> "VIBRANT"; + case EXPRESSIVE -> "EXPRESSIVE"; + case RAINBOW -> "RAINBOW"; + case FRUIT_SALAD -> "FRUIT_SALAD"; + case CONTENT -> "CONTENT"; + case MONOCHROMATIC -> "MONOCHROMATIC"; + case CLOCK -> "CLOCK"; + case CLOCK_VIBRANT -> "CLOCK_VIBRANT"; + default -> throw new IllegalArgumentException("Invalid style value: " + style); + }; + } + + /** + * Returns the style value corresponding to the given style name. + * + * @param styleName The name of the style. + * @return The style value. + * @throws IllegalArgumentException if the style name is invalid. + */ + public static @Type int valueOf(@Nullable String styleName) { + return switch (styleName) { + case "SPRITZ" -> SPRITZ; + case "TONAL_SPOT" -> TONAL_SPOT; + case "VIBRANT" -> VIBRANT; + case "EXPRESSIVE" -> EXPRESSIVE; + case "RAINBOW" -> RAINBOW; + case "FRUIT_SALAD" -> FRUIT_SALAD; + case "CONTENT" -> CONTENT; + case "MONOCHROMATIC" -> MONOCHROMATIC; + case "CLOCK" -> CLOCK; + case "CLOCK_VIBRANT" -> CLOCK_VIBRANT; + default -> throw new IllegalArgumentException("Invalid style name: " + styleName); + }; + } + + /** + * Returns the name of the given style. This method is equivalent to {@link #toString(int)}. + * + * @param style The style value. + * @return The name of the style. + */ + @NonNull + public static String name(@Type int style) { + return toString(style); + } + + /** + * Returns an array containing all the style values. + * + * @return An array of all style values. + */ + public static int[] values() { + return new int[]{ + SPRITZ, + TONAL_SPOT, + VIBRANT, + EXPRESSIVE, + RAINBOW, + FRUIT_SALAD, + CONTENT, + MONOCHROMATIC, + CLOCK, + CLOCK_VIBRANT + }; + } +} diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index 739908ef0dfc..b4ca217539a3 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -129,12 +129,6 @@ public final class BatteryUsageStats implements Parcelable, Closeable { // Max window size. CursorWindow uses only as much memory as needed. private static final long BATTERY_CONSUMER_CURSOR_WINDOW_SIZE = 20_000_000; // bytes - /** - * Used by tests to ensure all BatteryUsageStats instances are closed. - */ - @VisibleForTesting - public static boolean DEBUG_INSTANCE_COUNT; - private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000; private static final int[] UID_USAGE_TIME_PROCESS_STATES = { @@ -1267,11 +1261,16 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } } + /* + * Used by tests to ensure all BatteryUsageStats instances are closed. + */ + private static volatile boolean sInstanceLeakDetectionEnabled; + @GuardedBy("BatteryUsageStats.class") private static Map<CursorWindow, Exception> sInstances; private static void onCursorWindowAllocated(CursorWindow window) { - if (!DEBUG_INSTANCE_COUNT) { + if (!sInstanceLeakDetectionEnabled) { return; } @@ -1284,7 +1283,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } private static void onCursorWindowReleased(CursorWindow window) { - if (!DEBUG_INSTANCE_COUNT) { + if (!sInstanceLeakDetectionEnabled) { return; } @@ -1294,12 +1293,26 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } /** + * Enables detection of leaked BatteryUsageStats instances, meaning instances that are created + * but not closed during the test execution. + */ + @VisibleForTesting + public static void enableInstanceLeakDetection() { + sInstanceLeakDetectionEnabled = true; + synchronized (BatteryUsageStats.class) { + if (sInstances != null) { + sInstances.clear(); + } + } + } + + /** * Used by tests to ensure all BatteryUsageStats instances are closed. */ @VisibleForTesting public static void assertAllInstancesClosed() { - if (!DEBUG_INSTANCE_COUNT) { - throw new IllegalStateException("DEBUG_INSTANCE_COUNT is false"); + if (!sInstanceLeakDetectionEnabled) { + throw new IllegalStateException("Instance leak detection is not enabled"); } synchronized (BatteryUsageStats.class) { diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 09137c3a7b65..edfb78e59fe3 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -52,13 +52,6 @@ flag { } flag { - name: "binary_transparency_sepolicy_hash" - namespace: "hardware_backed_security" - description: "Collect sepolicy hash from sysfs" - bug: "308471499" -} - -flag { name: "frp_enforcement" is_exported: true namespace: "hardware_backed_security" diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 6decd6d3a603..0558858895b8 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -257,6 +257,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } @Override + public boolean willUpdateSurface() { + return !mFinished && !mCancelled; + } + + @Override public @AnimationType int getAnimationType() { return mAnimationType; } diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java index 4f102da4692a..968181b1723d 100644 --- a/core/java/android/view/InsetsAnimationControlRunner.java +++ b/core/java/android/view/InsetsAnimationControlRunner.java @@ -55,6 +55,12 @@ public interface InsetsAnimationControlRunner { void updateSurfacePosition(SparseArray<InsetsSourceControl> controls); /** + * Returns {@code true} if this runner will keep playing the animation and updating the surface. + * {@code false} otherwise. + */ + boolean willUpdateSurface(); + + /** * Cancels the animation. */ void cancel(); diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java index 8c2c4951a9f7..8acb46dcc0a4 100644 --- a/core/java/android/view/InsetsAnimationThreadControlRunner.java +++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java @@ -89,7 +89,7 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro } }; - private SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() { + private final SurfaceParamsApplier mSurfaceParamsApplier = new SurfaceParamsApplier() { private final float[] mTmpFloat9 = new float[9]; @@ -170,6 +170,17 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro @Override @UiThread + public boolean willUpdateSurface() { + synchronized (mControl) { + // This is called from the UI thread, however, applyChangeInsets would be called on the + // animation thread, so we need this critical section to ensure this is not called + // during applyChangeInsets. See: scheduleApplyChangeInsets. + return mControl.willUpdateSurface(); + } + } + + @Override + @UiThread public void cancel() { InsetsAnimationThread.getHandler().post(mControl::cancel); } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 4c578fb93600..f7ffc1e1a103 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1747,9 +1747,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mTypesBeingCancelled |= types; try { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { - InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; - if ((control.getTypes() & types) != 0) { - cancelAnimation(control, true /* invokeCallback */); + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + if ((runner.getTypes() & types) != 0) { + cancelAnimation(runner, true /* invokeCallback */); } } if ((types & ime()) != 0) { @@ -1788,11 +1788,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW); ImeTracker.forLogging().onShown(statsToken); } else { - ImeTracker.forLogging().onProgress(statsToken, - ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE); // The requestedVisibleTypes are only send at the end of the hide animation. // Therefore, the requested is not finished at this point. if (!Flags.refactorInsetsController()) { + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE); ImeTracker.forLogging().onHidden(statsToken); } } @@ -1807,10 +1807,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation void notifyControlRevoked(InsetsSourceConsumer consumer) { final @InsetsType int type = consumer.getType(); for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { - InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; - control.notifyControlRevoked(type); - if (control.getControllingTypes() == 0) { - cancelAnimation(control, true /* invokeCallback */); + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + runner.notifyControlRevoked(type); + if (runner.getControllingTypes() == 0) { + cancelAnimation(runner, true /* invokeCallback */); } } if (type == ime()) { @@ -1823,38 +1823,38 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } - private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) { + private void cancelAnimation(InsetsAnimationControlRunner runner, boolean invokeCallback) { if (invokeCallback) { - ImeTracker.forLogging().onCancelled(control.getStatsToken(), + ImeTracker.forLogging().onCancelled(runner.getStatsToken(), ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL); - control.cancel(); + runner.cancel(); } else { // Succeeds if invokeCallback is false (i.e. when called from notifyFinished). - ImeTracker.forLogging().onProgress(control.getStatsToken(), + ImeTracker.forLogging().onProgress(runner.getStatsToken(), ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL); } if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "cancelAnimation of types: %d, animType: %d, host: %s", - control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle())); + runner.getTypes(), runner.getAnimationType(), mHost.getRootViewTitle())); } @InsetsType int removedTypes = 0; for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { RunningAnimation runningAnimation = mRunningAnimations.get(i); - if (runningAnimation.runner == control) { + if (runningAnimation.runner == runner) { mRunningAnimations.remove(i); - removedTypes = control.getTypes(); + removedTypes = runner.getTypes(); if (invokeCallback) { dispatchAnimationEnd(runningAnimation.runner.getAnimation()); } else { if (Flags.refactorInsetsController()) { if ((removedTypes & ime()) != 0 - && control.getAnimationType() == ANIMATION_TYPE_HIDE) { + && runner.getAnimationType() == ANIMATION_TYPE_HIDE) { if (mHost != null) { // if the (hide) animation is cancelled, the // requestedVisibleTypes should be reported at this point. reportRequestedVisibleTypes(!Flags.reportAnimatingInsetsTypes() - ? control.getStatsToken() : null); + ? runner.getStatsToken() : null); mHost.getInputMethodManager().removeImeSurface( mHost.getWindowToken()); } @@ -1869,9 +1869,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (mHost != null) { final boolean dispatchStatsToken = Flags.reportAnimatingInsetsTypes() && (removedTypes & ime()) != 0 - && control.getAnimationType() == ANIMATION_TYPE_HIDE; + && runner.getAnimationType() == ANIMATION_TYPE_HIDE; mHost.updateAnimatingTypes(mAnimatingTypes, - dispatchStatsToken ? control.getStatsToken() : null); + dispatchStatsToken ? runner.getStatsToken() : null); } } @@ -1959,14 +1959,30 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @VisibleForTesting(visibility = PACKAGE) public @AnimationType int getAnimationType(@InsetsType int type) { for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { - InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner; - if (control.controlsType(type)) { + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + if (runner.controlsType(type)) { return mRunningAnimations.get(i).type; } } return ANIMATION_TYPE_NONE; } + /** + * Returns {@code true} if there is an animation which controls the given {@link InsetsType} and + * the runner is still playing the surface animation. + * + * @see InsetsAnimationControlRunner#willUpdateSurface() + */ + boolean hasSurfaceAnimation(@InsetsType int type) { + for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { + final InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner; + if (runner.controlsType(type) && runner.willUpdateSurface()) { + return true; + } + } + return false; + } + @VisibleForTesting(visibility = PACKAGE) public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { final @InsetsType int requestedVisibleTypes = diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java index 5262751cc6ed..6356be262cc4 100644 --- a/core/java/android/view/InsetsResizeAnimationRunner.java +++ b/core/java/android/view/InsetsResizeAnimationRunner.java @@ -233,6 +233,11 @@ public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner } @Override + public boolean willUpdateSurface() { + return false; + } + + @Override public boolean hasZeroInsetsIme() { return false; } diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index 945975a88cd5..1a750a3f89c4 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -17,7 +17,6 @@ package android.view; import static android.view.InsetsController.ANIMATION_TYPE_NONE; -import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.AnimationType; import static android.view.InsetsController.DEBUG; import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE; @@ -201,9 +200,8 @@ public class InsetsSourceConsumer { } // If there is no animation controlling the leash, make sure the visibility and the - // position is up-to-date. Note: ANIMATION_TYPE_RESIZE doesn't control the leash. - final int animType = mController.getAnimationType(mType); - if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) { + // position is up-to-date. + if (!mController.hasSurfaceAnimation(mType)) { applyRequestedVisibilityAndPositionToControl(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index f32ce6f1d6e4..1213d173ab3b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5179,9 +5179,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * This lives here since it's only valid for interactive views. This list is null * until its first use. */ - private List<Rect> mSystemGestureExclusionRects = null; - private List<Rect> mKeepClearRects = null; - private List<Rect> mUnrestrictedKeepClearRects = null; + private ArrayList<Rect> mSystemGestureExclusionRects = null; + private ArrayList<Rect> mKeepClearRects = null; + private ArrayList<Rect> mUnrestrictedKeepClearRects = null; private boolean mPreferKeepClear = false; private Rect mHandwritingArea = null; @@ -12891,21 +12891,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final ListenerInfo info = getListenerInfo(); final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() - || !Objects.equals(info.mSystemGestureExclusionRects, rects); - if (info.mSystemGestureExclusionRects != null) { - if (rectsChanged) { - info.mSystemGestureExclusionRects.clear(); - info.mSystemGestureExclusionRects.addAll(rects); - } - } else { - info.mSystemGestureExclusionRects = new ArrayList<>(rects); + || !Objects.deepEquals(info.mSystemGestureExclusionRects, rects); + if (info.mSystemGestureExclusionRects == null) { + info.mSystemGestureExclusionRects = new ArrayList<>(); } if (rectsChanged) { + deepCopyRectsObjectRecycling(info.mSystemGestureExclusionRects, rects); updatePositionUpdateListener(); postUpdate(this::updateSystemGestureExclusionRects); } } + private void deepCopyRectsObjectRecycling(@NonNull ArrayList<Rect> dest, List<Rect> src) { + dest.ensureCapacity(src.size()); + for (int i = 0; i < src.size(); i++) { + if (i < dest.size()) { + // Replace if there is an old rect to refresh + dest.get(i).set(src.get(i)); + } else { + // Add a rect if the list enlarged + dest.add(Rect.copyOrNull(src.get(i))); + } + } + while (dest.size() > src.size()) { + // Remove elements if the list shrank + dest.removeLast(); + } + } + private void updatePositionUpdateListener() { final ListenerInfo info = getListenerInfo(); if (getSystemGestureExclusionRects().isEmpty() @@ -13031,14 +13044,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setPreferKeepClearRects(@NonNull List<Rect> rects) { final ListenerInfo info = getListenerInfo(); - if (info.mKeepClearRects != null) { - info.mKeepClearRects.clear(); - info.mKeepClearRects.addAll(rects); - } else { - info.mKeepClearRects = new ArrayList<>(rects); + final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() + || !Objects.deepEquals(info.mKeepClearRects, rects); + if (info.mKeepClearRects == null) { + info.mKeepClearRects = new ArrayList<>(); + } + if (rectsChanged) { + deepCopyRectsObjectRecycling(info.mKeepClearRects, rects); + updatePositionUpdateListener(); + postUpdate(this::updateKeepClearRects); } - updatePositionUpdateListener(); - postUpdate(this::updateKeepClearRects); } /** @@ -13076,14 +13091,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS) public final void setUnrestrictedPreferKeepClearRects(@NonNull List<Rect> rects) { final ListenerInfo info = getListenerInfo(); - if (info.mUnrestrictedKeepClearRects != null) { - info.mUnrestrictedKeepClearRects.clear(); - info.mUnrestrictedKeepClearRects.addAll(rects); - } else { - info.mUnrestrictedKeepClearRects = new ArrayList<>(rects); + final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() + || !Objects.deepEquals(info.mUnrestrictedKeepClearRects, rects); + if (info.mUnrestrictedKeepClearRects == null) { + info.mUnrestrictedKeepClearRects = new ArrayList<>(); + } + if (rectsChanged) { + deepCopyRectsObjectRecycling(info.mUnrestrictedKeepClearRects, rects); + updatePositionUpdateListener(); + postUpdate(this::updateKeepClearRects); } - updatePositionUpdateListener(); - postUpdate(this::updateKeepClearRects); } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4e3ff9063179..9a62045f3435 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -189,6 +189,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.hardware.SyncFence; @@ -292,6 +293,7 @@ import com.android.internal.os.SomeArgs; import com.android.internal.policy.DecorView; import com.android.internal.policy.PhoneFallbackEventHandler; import com.android.internal.protolog.ProtoLog; +import com.android.internal.util.ContrastColorUtil; import com.android.internal.util.FastPrintWriter; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.RootViewSurfaceTaker; @@ -2078,12 +2080,21 @@ public final class ViewRootImpl implements ViewParent, // preference for dark mode in configuration.uiMode. Instead, we assume that both // force invert and the system's dark theme are enabled. if (shouldApplyForceInvertDark()) { - final boolean isLightTheme = - a.getBoolean(R.styleable.Theme_isLightTheme, false); - // TODO: b/372558459 - Also check the background ColorDrawable color lightness // TODO: b/368725782 - Use hwui color area detection instead of / in // addition to these heuristics. - if (isLightTheme) { + final boolean isLightTheme = + a.getBoolean(R.styleable.Theme_isLightTheme, false); + final boolean isBackgroundColorLight; + if (mView != null && mView.getBackground() + instanceof ColorDrawable colorDrawable) { + isBackgroundColorLight = + !ContrastColorUtil.isColorDarkLab(colorDrawable.getColor()); + } else { + // Treat unknown as light, so that only isLightTheme is used to determine + // force dark treatment. + isBackgroundColorLight = true; + } + if (isLightTheme && isBackgroundColorLight) { return ForceDarkType.FORCE_INVERT_COLOR_DARK; } else { return ForceDarkType.NONE; @@ -10270,6 +10281,8 @@ public final class ViewRootImpl implements ViewParent, try { mWindowSession.notifyImeWindowVisibilityChangedFromClient(mWindow, visible, statsToken); } catch (RemoteException e) { + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_NOTIFY_IME_VISIBILITY_CHANGED); e.rethrowFromSystemServer(); } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 9d21f1aff0c3..1ba3a74b8b2b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -80,9 +80,6 @@ import static android.view.WindowLayoutParamsProto.WINDOW_ANIMATIONS; import static android.view.WindowLayoutParamsProto.X; import static android.view.WindowLayoutParamsProto.Y; -import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; - import android.Manifest.permission; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; @@ -4549,29 +4546,6 @@ public interface WindowManager extends ViewManager { public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3; /** - * Input feature used to indicate that the system should send power key events to this - * window when it's in the foreground. The window can override the double press power key - * gesture behavior. - * - * A double press gesture is defined as two - * {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)} events within a time span defined by - * {@link ViewConfiguration#getMultiPressTimeout()}. - * - * Note: While the window may receive all power key {@link KeyEvent}s, it can only - * override the double press gesture behavior. The system will perform default behavior for - * single, long-press and other multi-press gestures, regardless of if the app handles the - * key or not. - * - * To override the default behavior for double press, the app must return true for the - * second {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)}. If the app returns false, the - * system behavior will be performed for double press. - * @hide - */ - @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public static final int - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS = 1 << 4; - - /** * An internal annotation for flags that can be specified to {@link #inputFeatures}. * * NOTE: These are not the same as {@link android.os.InputConfig} flags. @@ -4583,8 +4557,7 @@ public interface WindowManager extends ViewManager { INPUT_FEATURE_NO_INPUT_CHANNEL, INPUT_FEATURE_DISABLE_USER_ACTIVITY, INPUT_FEATURE_SPY, - INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY }) public @interface InputFeatureFlags { } @@ -4874,44 +4847,6 @@ public interface WindowManager extends ViewManager { } /** - * Specifies if the system should send power key events to this window when it's in the - * foreground, with only the double tap gesture behavior being overrideable. - * - * @param enabled if true, the system should send power key events to this window when it's - * in the foreground, with only the power key double tap gesture being - * overrideable. - * @hide - */ - @SystemApi - @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - @FlaggedApi(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void setReceivePowerKeyDoublePressEnabled(boolean enabled) { - if (enabled) { - inputFeatures - |= INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; - } else { - inputFeatures - &= ~INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; - } - } - - /** - * Returns whether or not the system should send power key events to this window when it's - * in the foreground, with only the double tap gesture being overrideable. - * - * @return if the system should send power key events to this window when it's in the - * foreground, with only the double tap gesture being overrideable. - * @hide - */ - @SystemApi - @RequiresPermission(permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - @FlaggedApi(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public boolean isReceivePowerKeyDoublePressEnabled() { - return (inputFeatures - & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; - } - - /** * Specifies that the window should be considered a trusted system overlay. Trusted system * overlays are ignored when considering whether windows are obscured during input * dispatch. Requires the {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} @@ -6312,16 +6247,6 @@ public interface WindowManager extends ViewManager { inputFeatures &= ~INPUT_FEATURE_SPY; features.add("INPUT_FEATURE_SPY"); } - if (overridePowerKeyBehaviorInFocusedWindow()) { - if ((inputFeatures - & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) - != 0) { - inputFeatures - &= - ~INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; - features.add("INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS"); - } - } if (inputFeatures != 0) { features.add(Integer.toHexString(inputFeatures)); } diff --git a/core/java/android/view/XrWindowProperties.java b/core/java/android/view/XrWindowProperties.java index 23021a563393..c02d7a9bb8c5 100644 --- a/core/java/android/view/XrWindowProperties.java +++ b/core/java/android/view/XrWindowProperties.java @@ -27,7 +27,7 @@ public final class XrWindowProperties { private XrWindowProperties() {} /** - * Both Application and activity level + * Application and Activity level * {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to * inform the system of the activity launch mode in XR. When it is declared at the application * level, all activities are set to the defined value, unless it is overridden at the activity @@ -105,7 +105,7 @@ public final class XrWindowProperties { "XR_ACTIVITY_START_MODE_HOME_SPACE"; /** - * Both Application and activity level + * Application and Activity level * {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to * inform the system of the type of safety boundary recommended for the activity. When it is * declared at the application level, all activities are set to the defined value, unless it is @@ -156,4 +156,30 @@ public final class XrWindowProperties { */ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) public static final String XR_BOUNDARY_TYPE_LARGE = "XR_BOUNDARY_TYPE_LARGE"; + + /** + * Application and Activity level + * {@link android.content.pm.PackageManager.Property PackageManager.Property} to inform the + * system if it should play a system provided default animation when the app requests to enter + * or exit <a + * href="https://developer.android.com/develop/xr/jetpack-xr-sdk/transition-home-space-to-full-space">managed + * full space mode</a> in XR. When set to {@code true}, the system provided default animation is + * not played and the app is responsible for playing a custom enter or exit animation. When it + * is declared at the application level, all activities are set to the defined value, unless it + * is overridden at the activity level. + * + * <p>The default value is {@code false}. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION" + * android:value="false|true"/> + * </application> + * </pre> + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION = + "android.window.PROPERTY_XR_USES_CUSTOM_FULL_SPACE_MANAGED_ANIMATION"; } diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 0814e23eea87..816d2debf87e 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -422,7 +422,7 @@ public class AutofillFeatureFlags { * * @hide */ - public static final boolean DEFAULT_SESSION_FILL_EVENT_HISTORY_ENABLED = false; + public static final boolean DEFAULT_SESSION_FILL_EVENT_HISTORY_ENABLED = true; /** * @hide diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index b1ba8b32d2f4..64f41c7a2987 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -232,6 +232,8 @@ public interface ImeTracker { PHASE_WM_NOTIFY_HIDE_ANIMATION_FINISHED, PHASE_WM_UPDATE_DISPLAY_WINDOW_ANIMATING_TYPES, PHASE_CLIENT_ON_CONTROLS_CHANGED, + PHASE_SERVER_IME_INVOKER, + PHASE_SERVER_CLIENT_INVOKER, }) @Retention(RetentionPolicy.SOURCE) @interface Phase {} @@ -473,6 +475,10 @@ public interface ImeTracker { /** InsetsController received a control for the IME. */ int PHASE_CLIENT_ON_CONTROLS_CHANGED = ImeProtoEnums.PHASE_CLIENT_ON_CONTROLS_CHANGED; + /** Reached the IME invoker on the server. */ + int PHASE_SERVER_IME_INVOKER = ImeProtoEnums.PHASE_SERVER_IME_INVOKER; + /** Reached the IME client invoker on the server. */ + int PHASE_SERVER_CLIENT_INVOKER = ImeProtoEnums.PHASE_SERVER_CLIENT_INVOKER; /** * Called when an IME request is started. diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index fdaa41c63343..983be682b8aa 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -55,14 +55,14 @@ public enum DesktopModeFlags { ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS( Flags::enableCaptionCompatInsetForceConsumptionAlways, true), ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true), - ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, false), + ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, true), ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX( Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix, true), ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), - ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, false), + ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, true), ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX( Flags::enableDesktopIndicatorInSeparateThreadBugfix, false), ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX( @@ -111,7 +111,7 @@ public enum DesktopModeFlags { ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), - ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, false), + ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, true), ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS(Flags::enableModalsFullscreenWithPermission, true), ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS( diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index 0801dd8c0bd8..fc74a179f66e 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -119,7 +119,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { * Callback indicating that the given document has been deleted or moved. This gives * the provider a hook to revoke the uri permissions. */ - protected void onDocIdDeleted(String docId) { + protected void onDocIdDeleted(String docId, boolean shouldRevokeUriPermission) { // Default is no-op } @@ -292,7 +292,6 @@ public abstract class FileSystemProvider extends DocumentsProvider { final String afterDocId = getDocIdForFile(after); onDocIdChanged(docId); - onDocIdDeleted(docId); onDocIdChanged(afterDocId); final File afterVisibleFile = getFileForDocId(afterDocId, true); @@ -301,6 +300,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { updateMediaStore(getContext(), afterVisibleFile); if (!TextUtils.equals(docId, afterDocId)) { + // DocumentsProvider handles the revoking / granting uri permission for the docId and + // the afterDocId in the renameDocument case. Don't need to call revokeUriPermission + // for the docId here. + onDocIdDeleted(docId, /* shouldRevokeUriPermission */ false); return afterDocId; } else { return null; @@ -324,7 +327,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { final String docId = getDocIdForFile(after); onDocIdChanged(sourceDocumentId); - onDocIdDeleted(sourceDocumentId); + onDocIdDeleted(sourceDocumentId, /* shouldRevokeUriPermission */ true); onDocIdChanged(docId); // update the database updateMediaStore(getContext(), visibleFileBefore); @@ -362,7 +365,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { } onDocIdChanged(docId); - onDocIdDeleted(docId); + onDocIdDeleted(docId, /* shouldRevokeUriPermission */ true); updateMediaStore(getContext(), visibleFile); } diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java index 4d5e67ab8fde..9cdfc02e2e28 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java +++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java @@ -305,6 +305,8 @@ public final class InputMethodDebug { return "HIDE_INPUT_TARGET_CHANGED"; case SoftInputShowHideReason.HIDE_WINDOW_LOST_FOCUS: return "HIDE_WINDOW_LOST_FOCUS"; + case SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER: + return "IME_REQUESTED_CHANGED_LISTENER"; default: return "Unknown=" + reason; } diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java index cf0580c2f021..8b4371ea8478 100644 --- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java +++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java @@ -92,6 +92,7 @@ import java.lang.annotation.Retention; SoftInputShowHideReason.SHOW_INPUT_TARGET_CHANGED, SoftInputShowHideReason.HIDE_INPUT_TARGET_CHANGED, SoftInputShowHideReason.HIDE_WINDOW_LOST_FOCUS, + SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER, }) public @interface SoftInputShowHideReason { /** Default, undefined reason. */ @@ -422,4 +423,10 @@ public @interface SoftInputShowHideReason { /** Hide soft input when the window lost focus. */ int HIDE_WINDOW_LOST_FOCUS = ImeProtoEnums.REASON_HIDE_WINDOW_LOST_FOCUS; + + /** + * Show / Hide soft input by + * {@link com.android.server.wm.WindowManagerInternal.OnImeRequestedChangedListener} + */ + int IME_REQUESTED_CHANGED_LISTENER = ImeProtoEnums.REASON_IME_REQUESTED_CHANGED_LISTENER; } diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java index fed8fe3b4cc0..b20f6d225b69 100644 --- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java +++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java @@ -27,13 +27,11 @@ public class KeyInterceptionInfo { // Debug friendly name to help identify the window public final String windowTitle; public final int windowOwnerUid; - public final int inputFeaturesFlags; - public KeyInterceptionInfo(int type, int flags, String title, int uid, int inputFeaturesFlags) { + public KeyInterceptionInfo(int type, int flags, String title, int uid) { layoutParamsType = type; layoutParamsPrivateFlags = flags; windowTitle = title; windowOwnerUid = uid; - this.inputFeaturesFlags = inputFeaturesFlags; } } diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java index 0fd139188665..c68f107951ac 100644 --- a/core/java/com/android/internal/util/ContrastColorUtil.java +++ b/core/java/com/android/internal/util/ContrastColorUtil.java @@ -41,8 +41,6 @@ import android.text.style.TextAppearanceSpan; import android.util.Log; import android.util.Pair; -import com.android.internal.annotations.VisibleForTesting; - import java.util.Arrays; import java.util.WeakHashMap; @@ -381,6 +379,13 @@ public class ContrastColorUtil { return calculateLuminance(color) <= 0.17912878474; } + /** Like {@link #isColorDark(int)} but converts to LAB before checking the L component. */ + public static boolean isColorDarkLab(int color) { + final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); + ColorUtilsFromCompat.colorToLAB(color, result); + return result[0] < 50; + } + private int processColor(int color) { return Color.argb(Color.alpha(color), 255 - Color.red(color), diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index d35072fc10c3..6f1d72944a55 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -41,7 +41,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; -import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.hardware.input.InputManagerGlobal; import android.os.Build; @@ -77,7 +76,6 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.List; /** @@ -240,8 +238,6 @@ public class LockPatternUtils { private final SparseLongArray mLockoutDeadlines = new SparseLongArray(); private Boolean mHasSecureLockScreen; - private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>(); - /** * Use {@link TrustManager#isTrustUsuallyManaged(int)}. * @@ -363,22 +359,6 @@ public class LockPatternUtils { return mUserManager; } - private UserManager getUserManager(int userId) { - UserHandle userHandle = UserHandle.of(userId); - if (mUserManagerCache.containsKey(userHandle)) { - return mUserManagerCache.get(userHandle); - } - - try { - Context userContext = mContext.createPackageContextAsUser("system", 0, userHandle); - UserManager userManager = userContext.getSystemService(UserManager.class); - mUserManagerCache.put(userHandle, userManager); - return userManager; - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException("Failed to create context for user " + userHandle, e); - } - } - private TrustManager getTrustManager() { TrustManager trust = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE); if (trust == null) { @@ -966,7 +946,7 @@ public class LockPatternUtils { */ public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled, LockscreenCredential profilePassword) { - if (!isCredentialSharableWithParent(userHandle)) { + if (!isCredentialShareableWithParent(userHandle)) { return; } try { @@ -985,7 +965,7 @@ public class LockPatternUtils { * credential is not shareable with its parent, or a non-profile user. */ public boolean isSeparateProfileChallengeEnabled(int userHandle) { - return isCredentialSharableWithParent(userHandle) && hasSeparateChallenge(userHandle); + return isCredentialShareableWithParent(userHandle) && hasSeparateChallenge(userHandle); } /** @@ -995,7 +975,7 @@ public class LockPatternUtils { * credential is not shareable with its parent, or a non-profile user. */ public boolean isProfileWithUnifiedChallenge(int userHandle) { - return isCredentialSharableWithParent(userHandle) && !hasSeparateChallenge(userHandle); + return isCredentialShareableWithParent(userHandle) && !hasSeparateChallenge(userHandle); } /** @@ -1020,8 +1000,13 @@ public class LockPatternUtils { return info != null && info.isManagedProfile(); } - private boolean isCredentialSharableWithParent(int userHandle) { - return getUserManager(userHandle).isCredentialSharableWithParent(); + private boolean isCredentialShareableWithParent(int userHandle) { + try { + return getUserManager().getUserProperties(UserHandle.of(userHandle)) + .isCredentialShareableWithParent(); + } catch (IllegalArgumentException e) { + return false; + } } /** diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index 5225ce878310..0eb7c4aee287 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -51,8 +51,7 @@ static const char* kPathAllowlist[] = { "/dev/blkio/tasks", "/metadata/aconfig/maps/system.package.map", "/metadata/aconfig/maps/system.flag.map", - "/metadata/aconfig/boot/system.val", - "/metadata/libprocessgroup/memcg_v2_max_activation_depth" // TODO Revert after go/android-memcgv2-exp b/386797433 + "/metadata/aconfig/boot/system.val" }; static const char kFdPath[] = "/proc/self/fd"; diff --git a/core/res/res/drawable/ic_standby.xml b/core/res/res/drawable/ic_standby.xml new file mode 100644 index 000000000000..6736f14f1377 --- /dev/null +++ b/core/res/res/drawable/ic_standby.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,600Q530,600 565,565Q600,530 600,480Q600,430 565,395Q530,360 480,360Q430,360 395,395Q360,430 360,480Q360,530 395,565Q430,600 480,600ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/> +</vector> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 828461c66a1f..cb4dd46e70fe 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2029,6 +2029,9 @@ <!-- Class name of WallpaperManagerService. --> <string name="config_wallpaperManagerServiceName" translatable="false">com.android.server.wallpaper.WallpaperManagerService</string> + <!-- True if live wallpapers can be supported in the deskop experience --> + <bool name="config_isLiveWallpaperSupportedInDesktopExperience">false</bool> + <!-- Specifies priority of automatic time sources. Suggestions from higher entries in the list take precedence over lower ones. See com.android.server.timedetector.TimeDetectorStrategy for available sources. --> @@ -3836,6 +3839,7 @@ "lockdown" = Lock down device until the user authenticates "logout" = Logout the current user "system_update" = Launch System Update screen + "standby" = Bring the device to standby --> <string-array translatable="false" name="config_globalActionsList"> <item>emergency</item> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d94d659446ac..da6ebe9e7ac0 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -739,6 +739,9 @@ <!-- label for screenshot item in power menu [CHAR LIMIT=24]--> <string name="global_action_screenshot">Screenshot</string> + <!-- label for standby item in power menu [CHAR LIMIT=24]--> + <string name="global_action_standby">Standby</string> + <!-- Take bug report menu title [CHAR LIMIT=30] --> <string name="bugreport_title">Bug report</string> <!-- Message in bugreport dialog describing what it does [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b9ca96f31cd4..ab219a595b09 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1958,6 +1958,7 @@ <java-symbol type="string" name="global_action_voice_assist" /> <java-symbol type="string" name="global_action_assist" /> <java-symbol type="string" name="global_action_screenshot" /> + <java-symbol type="string" name="global_action_standby" /> <java-symbol type="string" name="invalidPuk" /> <java-symbol type="string" name="lockscreen_carrier_default" /> <java-symbol type="style" name="Animation.LockScreen" /> @@ -2281,6 +2282,7 @@ <java-symbol type="string" name="heavy_weight_notification_detail" /> <java-symbol type="string" name="image_wallpaper_component" /> <java-symbol type="string" name="fallback_wallpaper_component" /> + <java-symbol type="bool" name="config_isLiveWallpaperSupportedInDesktopExperience" /> <java-symbol type="string" name="input_method_binding_label" /> <java-symbol type="string" name="input_method_ime_switch_long_click_action_desc" /> <java-symbol type="string" name="launch_warning_original" /> @@ -3721,6 +3723,7 @@ <java-symbol type="drawable" name="ic_screenshot" /> <java-symbol type="drawable" name="ic_faster_emergency" /> <java-symbol type="drawable" name="ic_media_seamless" /> + <java-symbol type="drawable" name="ic_standby" /> <java-symbol type="drawable" name="emergency_icon" /> <java-symbol type="array" name="config_convert_to_emergency_number_map" /> diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 5774109e1451..1b7805c351db 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -16,8 +16,6 @@ package android.view; -import static android.app.UiModeManager.MODE_NIGHT_NO; -import static android.app.UiModeManager.MODE_NIGHT_YES; import static android.util.SequenceUtils.getInitSeq; import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING; import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; @@ -69,10 +67,9 @@ import static org.junit.Assume.assumeTrue; import android.annotation.NonNull; import android.app.Instrumentation; import android.app.UiModeManager; -import android.app.UiModeManager.ForceInvertType; import android.content.Context; +import android.graphics.Color; import android.graphics.ForceDarkType; -import android.graphics.ForceDarkType.ForceDarkTypeDef; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.os.Binder; @@ -101,8 +98,6 @@ import com.android.compatibility.common.util.TestUtils; import com.android.cts.input.BlockingQueueEventVerifier; import com.android.window.flags.Flags; -import com.google.common.truth.Expect; - import org.hamcrest.Matcher; import org.junit.After; import org.junit.AfterClass; @@ -131,8 +126,6 @@ public class ViewRootImplTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Rule - public final Expect mExpect = Expect.create(); private ViewRootImpl mViewRootImpl; private View mView; @@ -1516,29 +1509,83 @@ public class ViewRootImplTest { } @Test - @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR) - public void updateConfiguration_returnsExpectedForceDarkMode() { + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_systemLightMode_returnsNone() throws Exception { + waitForSystemNightModeActivated(false); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_systemNightModeAndDisableForceInvertColor_returnsNone() + throws Exception { waitForSystemNightModeActivated(true); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); + enableForceInvertColor(false); - waitForSystemNightModeActivated(false); + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void + determineForceDarkType_isLightThemeAndIsLightBackground_returnsForceInvertColorDark() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ true); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() + == ForceDarkType.FORCE_INVERT_COLOR_DARK)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_isLightThemeAndNotLightBackground_returnsNone() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ true, /* isLightBackground = */ false); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_notLightThemeAndIsLightBackground_returnsNone() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ true); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); + } + + @Test + @EnableFlags(FLAG_FORCE_INVERT_COLOR) + public void determineForceDarkType_notLightThemeAndNotLightBackground_returnsNone() + throws Exception { + // Set up configurations for force invert color + waitForSystemNightModeActivated(true); + enableForceInvertColor(true); + + setUpViewAttributes(/* isLightTheme= */ false, /* isLightBackground = */ false); + + TestUtils.waitUntil("Waiting for ForceDarkType to be ready", + () -> (mViewRootImpl.determineForceDarkType() == ForceDarkType.NONE)); } @Test @@ -1792,29 +1839,35 @@ public class ViewRootImplTest { sInstrumentation.waitForIdleSync(); } - private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled, - @ForceInvertType int expectedForceInvertType, - @ForceDarkTypeDef int expectedForceDarkType) { - var uiModeManager = sContext.getSystemService(UiModeManager.class); + private void enableForceInvertColor(boolean enabled) { ShellIdentityUtils.invokeWithShellPermissions(() -> { - uiModeManager.setApplicationNightMode( - isAppInNightMode ? MODE_NIGHT_YES : MODE_NIGHT_NO); Settings.Secure.putInt( sContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, - isForceInvertEnabled ? 1 : 0); + enabled ? 1 : 0 + ); }); + } - sInstrumentation.runOnMainSync(() -> - mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())); - try { - TestUtils.waitUntil("Waiting for force invert state changed", - () -> (uiModeManager.getForceInvertState() == expectedForceInvertType)); - } catch (Exception e) { - Log.e(TAG, "Unexpected error trying to apply force invert state. " + e); - e.printStackTrace(); - } + private void setUpViewAttributes(boolean isLightTheme, boolean isLightBackground) { + ShellIdentityUtils.invokeWithShellPermissions(() -> { + sContext.setTheme(isLightTheme ? android.R.style.Theme_DeviceDefault_Light + : android.R.style.Theme_DeviceDefault); + }); - mExpect.that(mViewRootImpl.determineForceDarkType()).isEqualTo(expectedForceDarkType); + sInstrumentation.runOnMainSync(() -> { + View view = new View(sContext); + WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( + TYPE_APPLICATION_OVERLAY); + layoutParams.token = new Binder(); + view.setLayoutParams(layoutParams); + if (isLightBackground) { + view.setBackgroundColor(Color.WHITE); + } else { + view.setBackgroundColor(Color.BLACK); + } + mViewRootImpl.setView(view, layoutParams, /* panelParentView= */ null); + mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()); + }); } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 1d8eed2749d5..62e14d368a1c 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -621,6 +621,8 @@ applications that come with the platform <permission name="android.permission.READ_COLOR_ZONES"/> <!-- Permission required for CTS test - CtsTextClassifierTestCases --> <permission name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE"/> + <!-- Permission required for CTS test - CtsSecurityTestCases --> + <permission name="android.permission.MANAGE_DEVICE_POLICY_MTE"/> </privapp-permissions> <privapp-permissions package="com.android.soundpicker"> diff --git a/data/keyboards/Android.bp b/data/keyboards/Android.bp index 69b29bd5c7d3..423b55bd85db 100644 --- a/data/keyboards/Android.bp +++ b/data/keyboards/Android.bp @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package { - default_applicable_licenses: ["Android-Apache-2.0"], -} - genrule { name: "validate_framework_keymaps", srcs: [ - "*.idc", - "*.kcm", "*.kl", + "*.kcm", + "*.idc", ], tools: ["validatekeymaps"], out: ["stamp"], @@ -37,6 +33,7 @@ prebuilt_usr_keylayout { srcs: [ "*.kl", ], + no_full_install: true, } prebuilt_usr_keychars { @@ -44,6 +41,7 @@ prebuilt_usr_keychars { srcs: [ "*.kcm", ], + no_full_install: true, } prebuilt_usr_idc { @@ -51,4 +49,5 @@ prebuilt_usr_idc { srcs: [ "*.idc", ], + no_full_install: true, } diff --git a/data/keyboards/keyboards.mk b/data/keyboards/keyboards.mk index 47bc63268754..c7ce8cd6693a 100644 --- a/data/keyboards/keyboards.mk +++ b/data/keyboards/keyboards.mk @@ -14,7 +14,9 @@ # Warning: this is actually a product definition, to be inherited from -PRODUCT_PACKAGES += \ - keylayout_data \ - keychars_data \ - idc_data +PRODUCT_COPY_FILES := \ + $(call find-copy-subdir-files,*.kl,$(LOCAL_PATH),system/usr/keylayout) \ + $(call find-copy-subdir-files,*.kcm,$(LOCAL_PATH),system/usr/keychars) \ + $(call find-copy-subdir-files,*.idc,$(LOCAL_PATH),system/usr/idc) + + diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 08cda7b94a78..086c8a5651c3 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -51,7 +51,6 @@ <item name="android:clickable">true</item> <item name="android:focusable">true</item> <item name="android:orientation">horizontal</item> - <item name="android:background">?android:attr/selectableItemBackground</item> </style> <style name="DesktopModeHandleMenuActionButtonImage"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index b3c25d495002..ad509bcc1ceb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -766,6 +766,7 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, + @ShellAnimationThread Handler animHandler, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, HomeTransitionObserver homeTransitionObserver, FocusTransitionObserver focusTransitionObserver) { @@ -775,7 +776,7 @@ public abstract class WMShellBaseModule { } return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer, pool, displayController, displayInsetsController, mainExecutor, mainHandler, - animExecutor, rootTaskDisplayAreaOrganizer, homeTransitionObserver, + animExecutor, animHandler, rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 1c5138f486e4..8bbe36dd6644 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -57,7 +57,7 @@ import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.windowdecor.tiling.SnapEventHandler; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -307,7 +307,8 @@ public class DesktopModeVisualIndicator { if (splitRightRegion.contains(x, y)) { result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR; } - if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() + && mDragStartState == DragStartState.FROM_FULLSCREEN) { if (calculateBubbleLeftRegion(layout).contains(x, y)) { result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR; } else if (calculateBubbleRightRegion(layout).contains(x, y)) { @@ -415,30 +416,59 @@ public class DesktopModeVisualIndicator { private List<Pair<Rect, IndicatorType>> initSmallTabletRegions(DisplayLayout layout, boolean isLeftRightSplit) { - boolean dragFromFullscreen = mDragStartState == DragStartState.FROM_FULLSCREEN; - boolean dragFromSplit = mDragStartState == DragStartState.FROM_SPLIT; - if (isLeftRightSplit && (dragFromFullscreen || dragFromSplit)) { + return switch (mDragStartState) { + case DragStartState.FROM_FULLSCREEN -> initSmallTabletRegionsFromFullscreen(layout, + isLeftRightSplit); + case DragStartState.FROM_SPLIT -> initSmallTabletRegionsFromSplit(layout, + isLeftRightSplit); + default -> Collections.emptyList(); + }; + } + + private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromFullscreen( + DisplayLayout layout, boolean isLeftRightSplit) { + + List<Pair<Rect, IndicatorType>> result = new ArrayList<>(); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR)); + result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR)); + } + + if (isLeftRightSplit) { int splitRegionWidth = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold); - return Arrays.asList( - new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR), - new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR), - new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth, - /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR), - new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth, - /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR), - new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR) // default to fullscreen - ); + result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR)); + result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR)); } - if (dragFromFullscreen) { - // If left/right split is not available, we can only drag fullscreen tasks - // TODO(b/401352409): add support for top/bottom split zones - return Arrays.asList( - new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR), - new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR), - new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR) // default to fullscreen - ); + // TODO(b/401352409): add support for top/bottom split zones + // default to fullscreen + result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR)); + return result; + } + + private List<Pair<Rect, IndicatorType>> initSmallTabletRegionsFromSplit(DisplayLayout layout, + boolean isLeftRightSplit) { + if (!isLeftRightSplit) { + // Dragging a top/bottom split is not supported on small tablets + return Collections.emptyList(); } - return Collections.emptyList(); + + List<Pair<Rect, IndicatorType>> result = new ArrayList<>(); + if (BubbleAnythingFlagHelper.enableBubbleAnything()) { + result.add(new Pair<>(calculateBubbleLeftRegion(layout), TO_BUBBLE_LEFT_INDICATOR)); + result.add(new Pair<>(calculateBubbleRightRegion(layout), TO_BUBBLE_RIGHT_INDICATOR)); + } + + int splitRegionWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.shared.R.dimen.drag_zone_h_split_from_app_width_fold); + result.add(new Pair<>(calculateSplitLeftRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_LEFT_INDICATOR)); + result.add(new Pair<>(calculateSplitRightRegion(layout, splitRegionWidth, + /* captionHeight= */ 0), TO_SPLIT_RIGHT_INDICATOR)); + // default to fullscreen + result.add(new Pair<>(new Rect(), TO_FULLSCREEN_INDICATOR)); + return result; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 51ef0ec60c3a..9ec1c7d65a6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -50,7 +50,9 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; +import android.os.Debug; import android.os.IBinder; +import android.util.Log; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -315,6 +317,20 @@ public class PipTransition extends PipTransitionController implements return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } + + TransitionInfo.Change pipActivityChange = PipTransitionUtils + .getDeferConfigActivityChange(info, pipChange.getTaskInfo().getToken()); + if (pipActivityChange == null) { + // Legacy-enter and swipe-pip-to-home filters did not resolve a scheduled PiP entry. + // Bounds-type enter animation is the last resort, and it requires a config-at-end + // activity amongst the list of changes. If no such change, something went wrong. + Log.wtf(TAG, String.format(""" + PipTransition.startAnimation didn't handle a scheduled PiP entry + transitionInfo=%s, + callers=%s""", info, Debug.getCallers(4))); + return false; + } + return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mExitViaExpandTransition) { @@ -839,26 +855,15 @@ public class PipTransition extends PipTransitionController implements return true; } - // Sometimes root PiP task can have TF children. These child containers can be collected - // even if they can promote to their parents: e.g. if they are marked as "organized". - // So we count the chain of containers under PiP task as one "real" changing target; - // iterate through changes bottom-to-top to properly identify parents. - int expectedTargetCount = 1; - WindowContainerToken lastPipChildToken = pipChange.getContainer(); - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - TransitionInfo.Change change = info.getChanges().get(i); - if (change == pipChange || change.getContainer() == null) continue; - if (change.getParent() != null && change.getParent().equals(lastPipChildToken)) { - // Allow an extra change since our pinned root task has a child. - ++expectedTargetCount; - lastPipChildToken = change.getContainer(); - } - } - - // If the only root task change in the changes list is a opening type PiP task, - // then this is legacy-enter PiP. - return info.getChanges().size() == expectedTargetCount - && TransitionUtil.isOpeningMode(pipChange.getMode()); + // #getEnterPipTransaction() always attempts to mark PiP activity as config-at-end one. + // However, the activity will only actually be marked config-at-end by Core if it is + // both isVisible and isVisibleRequested, which is when we can't run bounds animation. + // + // So we can use the absence of a config-at-end activity as a signal that we should run + // a legacy-enter PiP animation instead. + return TransitionUtil.isOpeningMode(pipChange.getMode()) + && PipTransitionUtils.getDeferConfigActivityChange( + info, pipChange.getContainer()) == null; } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index e9200834c5dd..5b6993863c5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -133,6 +133,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final DisplayController mDisplayController; private final Context mContext; private final Handler mMainHandler; + private final Handler mAnimHandler; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; @@ -171,6 +172,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull TransactionPool transactionPool, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, + @NonNull Handler animHandler, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, @NonNull InteractionJankMonitor interactionJankMonitor) { mDisplayController = displayController; @@ -179,6 +181,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mMainHandler = mainHandler; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mAnimHandler = animHandler; mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG); mCurrentUserId = UserHandle.myUserId(); mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); @@ -349,10 +352,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mAnimations.put(transition, animations); final boolean isTaskTransition = isTaskTransition(info); - if (isTaskTransition) { - mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext, - mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); - } final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; @@ -642,6 +641,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // now start animations. they are started on another thread, so we have to post them // *after* applying the startTransaction mAnimExecutor.execute(() -> { + if (isTaskTransition) { + mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext, + mAnimHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } for (int i = 0; i < animations.size(); ++i) { animations.get(i).start(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 3dc8733c879d..84724268cfc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -215,6 +215,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; + private final Handler mAnimHandler; private final TransitionPlayerImpl mPlayerImpl; private final DefaultTransitionHandler mDefaultTransitionHandler; private final RemoteTransitionHandler mRemoteTransitionHandler; @@ -319,11 +320,12 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, + @NonNull Handler animHandler, @NonNull HomeTransitionObserver homeTransitionObserver, @NonNull FocusTransitionObserver focusTransitionObserver) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, displayInsetsController, mainExecutor, mainHandler, animExecutor, - new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), + animHandler, new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), homeTransitionObserver, focusTransitionObserver); } @@ -338,6 +340,7 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, + @NonNull Handler animHandler, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, @NonNull HomeTransitionObserver homeTransitionObserver, @NonNull FocusTransitionObserver focusTransitionObserver) { @@ -345,11 +348,12 @@ public class Transitions implements RemoteCallable<Transitions>, mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mAnimHandler = animHandler; mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, displayController, displayInsetsController, pool, mainExecutor, mainHandler, - animExecutor, rootTDAOrganizer, InteractionJankMonitor.getInstance()); + animExecutor, mAnimHandler, rootTDAOrganizer, InteractionJankMonitor.getInstance()); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellCommandHandler = shellCommandHandler; mShellController = shellController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index cdadce57d610..71bb153e4b1e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -508,6 +508,9 @@ class HandleMenu( private val iconButtonRippleRadius = context.resources.getDimensionPixelSize( R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius ) + private val handleMenuCornerRadius = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_corner_radius + ) private val iconButtonDrawableInsetsBase = DrawableInsets( t = iconButtondrawableBaseInset, b = iconButtondrawableBaseInset, l = iconButtondrawableBaseInset, @@ -866,14 +869,21 @@ class HandleMenu( private fun bindMoreActionsPill(style: MenuStyle) { moreActionsPill.background.setTint(style.backgroundColor) - - arrayOf( + val buttons = arrayOf( screenshotBtn to SHOULD_SHOW_SCREENSHOT_BUTTON, newWindowBtn to shouldShowNewWindowButton, manageWindowBtn to shouldShowManageWindowsButton, changeAspectRatioBtn to shouldShowChangeAspectRatioButton, restartBtn to shouldShowRestartButton, - ).forEach { (button, shouldShow) -> + ) + val firstVisible = buttons.find { it.second }?.first + val lastVisible = buttons.findLast { it.second }?.first + + buttons.forEach { (button, shouldShow) -> + val topRadius = + if (button == firstVisible) handleMenuCornerRadius.toFloat() else 0f + val bottomRadius = + if (button == lastVisible) handleMenuCornerRadius.toFloat() else 0f button.apply { isGone = !shouldShow textView.apply { @@ -881,6 +891,13 @@ class HandleMenu( startMarquee() } iconView.imageTintList = ColorStateList.valueOf(style.textColor) + background = createBackgroundDrawable( + color = style.textColor, + cornerRadius = floatArrayOf( + topRadius, topRadius, topRadius, topRadius, + bottomRadius, bottomRadius, bottomRadius, bottomRadius + ), + drawableInsets = DrawableInsets()) } } } @@ -899,6 +916,10 @@ class HandleMenu( openInAppOrBrowserBtn.apply { contentDescription = btnText + background = createBackgroundDrawable( + color = style.textColor, + cornerRadius = handleMenuCornerRadius, + drawableInsets = DrawableInsets()) textView.apply { text = btnText setTextColor(style.textColor) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt index f08cfa987cc7..33e743016d0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt @@ -51,10 +51,20 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { */ fun createBackgroundDrawable( @ColorInt color: Int, cornerRadius: Int, drawableInsets: DrawableInsets +): Drawable = createBackgroundDrawable( + color, + FloatArray(8) { cornerRadius.toFloat() }, + drawableInsets) + +/** + * Creates a background drawable with specified color, corner radius, and insets. + */ +fun createBackgroundDrawable( + @ColorInt color: Int, cornerRadius: FloatArray, drawableInsets: DrawableInsets ): Drawable = LayerDrawable(arrayOf( ShapeDrawable().apply { shape = RoundRectShape( - FloatArray(8) { cornerRadius.toFloat() }, + cornerRadius, /* inset= */ null, /* innerRadii= */ null ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index 652fae01c1b2..a4052890f08a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -283,14 +283,32 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, ) - fun testDefaultIndicators_bubblesEnabled() { + fun testDefaultIndicators_enableBubbleToFullscreen() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(10f, 1500f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR) - result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + + // Check that bubble zones are not available from split + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + result = visualIndicator.updateIndicatorType(PointF(10f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + + // Check that bubble zones are not available from desktop + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) + result = visualIndicator.updateIndicatorType(PointF(10f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2390f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) } @Test @@ -298,7 +316,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, ) - fun testDefaultIndicators_foldable_leftRightSplit() { + fun testDefaultIndicators_foldable_enableBubbleToFullscreen_dragFromFullscreen() { setUpFoldable() createVisualIndicator( @@ -325,13 +343,47 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { result = visualIndicator.updateIndicatorType(foldRightBottom()) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING) + fun testDefaultIndicators_foldable_enableBubbleToFullscreen_dragFromSplit() { + setUpFoldable() createVisualIndicator( DesktopModeVisualIndicator.DragStartState.FROM_SPLIT, isSmallTablet = true, isLeftRightSplit = true, ) - result = visualIndicator.updateIndicatorType(foldCenter()) + var result = visualIndicator.updateIndicatorType(foldCenter()) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Check that bubbles are not available from split + result = visualIndicator.updateIndicatorType(foldLeftBottom()) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + + result = visualIndicator.updateIndicatorType(foldRightBottom()) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + } + + @Test + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING) + fun testDefaultIndicators_foldable_enableBubbleAnything_dragFromSplit() { + setUpFoldable() + + createVisualIndicator( + DesktopModeVisualIndicator.DragStartState.FROM_SPLIT, + isSmallTablet = true, + isLeftRightSplit = true, + ) + var result = visualIndicator.updateIndicatorType(foldCenter()) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 82373ff1bc41..64bd86134d92 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -167,6 +167,7 @@ public class StageCoordinatorTests extends ShellTestCase { private final TestShellExecutor mMainExecutor = new TestShellExecutor(); private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Handler mAnimHandler = mock(Handler.class); private final DisplayAreaInfo mDisplayAreaInfo = new DisplayAreaInfo(new MockToken().token(), DEFAULT_DISPLAY, 0); private final ActivityManager.RunningTaskInfo mMainChildTaskInfo = @@ -629,7 +630,7 @@ public class StageCoordinatorTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mTaskOrganizer, mTransactionPool, mock(DisplayController.class), - mDisplayInsetsController, mMainExecutor, mMainHandler, mAnimExecutor, + mDisplayInsetsController, mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); shellInit.init(); return t; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java index 6996d44af034..2dab39184247 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -100,7 +100,8 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { mTransitionHandler = new DefaultTransitionHandler( mContext, mShellInit, mDisplayController, mDisplayInsetsController, mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor, - mRootTaskDisplayAreaOrganizer, mock(InteractionJankMonitor.class)); + mock(Handler.class), mRootTaskDisplayAreaOrganizer, + mock(InteractionJankMonitor.class)); mShellInit.init(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 52634c08dafd..5d77766dc0db 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -88,6 +88,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final TestShellExecutor mMainExecutor = new TestShellExecutor(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Handler mAnimHandler = mock(Handler.class); private final DisplayController mDisplayController = mock(DisplayController.class); private final DisplayInsetsController mDisplayInsetsController = mock(DisplayInsetsController.class); @@ -105,7 +106,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { mDisplayInsetsController, mock(ShellInit.class)); mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class), mOrganizer, mTransactionPool, mDisplayController, mDisplayInsetsController, - mMainExecutor, mMainHandler, mAnimExecutor, mHomeTransitionObserver, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mHomeTransitionObserver, mock(FocusTransitionObserver.class)); mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 44bb2154f170..4dd9cab1d340 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -146,6 +146,7 @@ public class ShellTransitionTests extends ShellTestCase { private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Handler mAnimHandler = mock(Handler.class); private final DisplayInsetsController mDisplayInsets = mock(DisplayInsetsController.class); @@ -160,7 +161,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = mock(ShellInit.class); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); // One from Transitions, one from RootTaskDisplayAreaOrganizer verify(shellInit).addInitCallback(any(), eq(t)); @@ -173,7 +174,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellController shellController = mock(ShellController.class); final Transitions t = new Transitions(mContext, shellInit, shellController, mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); shellInit.init(); verify(shellController, times(1)).addExternalInterface( @@ -1318,7 +1319,7 @@ public class ShellTransitionTests extends ShellTestCase { final Transitions transitions = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class); @@ -1914,7 +1915,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mDisplayInsets, - mMainExecutor, mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mMainExecutor, mMainHandler, mAnimExecutor, mAnimHandler, + mock(HomeTransitionObserver.class), mock(FocusTransitionObserver.class)); shellInit.init(); return t; diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 7221f1ddeb7f..15e87f80ef64 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -62,6 +62,16 @@ flag { } flag { + name: "enable_fix_for_empty_system_routes_crash" + namespace: "media_better_together" + description: "Fixes a bug causing SystemUI to crash due to an empty system routes list in the routing framework." + bug: "357468728" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_suggested_device_api" is_exported: true namespace: "media_better_together" diff --git a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl index b5afa6afa5e0..6ac1656b77aa 100644 --- a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl @@ -47,7 +47,7 @@ interface IMediaQualityManager { void setPictureProfileAllowList(in List<String> packages, int userId); List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId); - SoundProfile createSoundProfile(in SoundProfile pp, int userId); + void createSoundProfile(in SoundProfile pp, int userId); void updateSoundProfile(in String id, in SoundProfile pp, int userId); void removeSoundProfile(in String id, int userId); boolean setDefaultSoundProfile(in String id, int userId); diff --git a/media/java/android/media/tv/extension/scan/IScanInterface.aidl b/media/java/android/media/tv/extension/scan/IScanInterface.aidl index b44d1d243150..ea6e8a1d4104 100644 --- a/media/java/android/media/tv/extension/scan/IScanInterface.aidl +++ b/media/java/android/media/tv/extension/scan/IScanInterface.aidl @@ -24,7 +24,7 @@ import android.os.Bundle; */ interface IScanInterface { IBinder createSession(int broadcastType, String countryCode, String operator, - in IScanListener listener); + in IScanListener listener, in Bundle optionalParams); Bundle getParameters(int broadcastType, String countryCode, String operator, in Bundle params); } diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index defbc1142adb..28b891ebc3c9 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -596,7 +596,10 @@ public class ExternalStorageProvider extends FileSystemProvider { } @Override - protected void onDocIdDeleted(String docId) { + protected void onDocIdDeleted(String docId, boolean shouldRevokeUriPermission) { + if (!shouldRevokeUriPermission) { + return; + } Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, docId); getContext().revokeUriPermission(uri, ~0); } diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml index 8037a8bb75be..8a234fa6ca9e 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_button_background_normal.xml @@ -17,8 +17,8 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false" - android:alpha="@dimen/material_emphasis_disabled_background" android:color="?attr/colorOnSurface"/> + android:alpha="@dimen/material_emphasis_disabled_background" android:color="@color/settingslib_materialColorPrimary"/> <item android:state_checked="true" android:color="?attr/colorContainerChecked"/> <item android:state_checkable="true" android:color="?attr/colorContainerUnchecked"/> - <item android:color="?attr/colorContainer" /> + <item android:color="@color/settingslib_materialColorPrimary" /> </selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml new file mode 100644 index 000000000000..43b236938956 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_high.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_high"/> + <item android:color="@color/settingslib_colorContentLevel_high" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml new file mode 100644 index 000000000000..b7a9d7c5175b --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_low.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_low"/> + <item android:color="@color/settingslib_colorContentLevel_low" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml new file mode 100644 index 000000000000..8e41cb03f4d1 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_medium.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_colorContentLevel_medium"/> + <item android:color="@color/settingslib_colorContentLevel_medium" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml new file mode 100644 index 000000000000..1dd5cdecfffc --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_filled_button_content_normal.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_materialColorOnPrimary"/> + <item android:color="@color/settingslib_materialColorOnPrimary" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml new file mode 100644 index 000000000000..3a06fb38d5d8 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_content.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled" android:color="@color/settingslib_materialColorOnSurface"/> + <item android:color="@color/settingslib_materialColorOnSurface" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml new file mode 100644 index 000000000000..8d0b65712d35 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/color/settingslib_banner_outline_button_stroke_normal.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:alpha="@dimen/material_emphasis_disabled_background" android:color="@color/settingslib_materialColorOutline"/> + <item android:color="@color/settingslib_materialColorOutline" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml b/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml index 09e07ccef683..cd9faecc49c4 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/values-v36/styles_expressive.xml @@ -64,7 +64,6 @@ <style name="Banner.PositiveButton.SettingsLib.Expressive" parent="@style/SettingsLibButtonStyle.Expressive.Filled.Extra"> - <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> <item name="materialSizeOverlay">@style/SizeOverlay.Material3Expressive.Button.Small</item> </style> diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java index c90a76a39510..dbd0f6424ff8 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java @@ -58,35 +58,42 @@ public class BannerMessagePreference extends Preference implements GroupSectionD HIGH(0, R.color.banner_background_attention_high, R.color.banner_accent_attention_high, - R.color.settingslib_banner_button_background_high), + R.color.settingslib_banner_button_background_high, + R.color.settingslib_banner_filled_button_content_high), MEDIUM(1, R.color.banner_background_attention_medium, R.color.banner_accent_attention_medium, - R.color.settingslib_banner_button_background_medium), + R.color.settingslib_banner_button_background_medium, + R.color.settingslib_banner_filled_button_content_medium), LOW(2, R.color.banner_background_attention_low, R.color.banner_accent_attention_low, - R.color.settingslib_banner_button_background_low), + R.color.settingslib_banner_button_background_low, + R.color.settingslib_banner_filled_button_content_low), NORMAL(3, R.color.banner_background_attention_normal, R.color.banner_accent_attention_normal, - R.color.settingslib_banner_button_background_normal); + R.color.settingslib_banner_button_background_normal, + R.color.settingslib_banner_filled_button_content_normal); // Corresponds to the enum value of R.attr.attentionLevel private final int mAttrValue; @ColorRes private final int mBackgroundColorResId; @ColorRes private final int mAccentColorResId; @ColorRes private final int mButtonBackgroundColorResId; + @ColorRes private final int mButtonContentColorResId; AttentionLevel( int attrValue, @ColorRes int backgroundColorResId, @ColorRes int accentColorResId, - @ColorRes int buttonBackgroundColorResId) { + @ColorRes int buttonBackgroundColorResId, + @ColorRes int buttonContentColorResId) { mAttrValue = attrValue; mBackgroundColorResId = backgroundColorResId; mAccentColorResId = accentColorResId; mButtonBackgroundColorResId = buttonBackgroundColorResId; + mButtonContentColorResId = buttonContentColorResId; } static AttentionLevel fromAttr(int attrValue) { @@ -109,6 +116,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD public @ColorRes int getButtonBackgroundColorResId() { return mButtonBackgroundColorResId; } + + public @ColorRes int getButtonContentColorResId() { + return mButtonContentColorResId; + } } private static final String TAG = "BannerPreference"; @@ -181,6 +192,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); final Context context = getContext(); + final Resources resources = context.getResources(); final TextView titleView = (TextView) holder.findViewById(R.id.banner_title); CharSequence title = getTitle(); @@ -200,7 +212,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD final Resources.Theme theme = context.getTheme(); @ColorInt final int accentColor = - context.getResources().getColor(mAttentionLevel.getAccentColorResId(), theme); + resources.getColor(mAttentionLevel.getAccentColorResId(), theme); final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon); if (iconView != null) { @@ -211,9 +223,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } else { iconView.setVisibility(View.VISIBLE); iconView.setImageDrawable( - icon == null - ? getContext().getDrawable(R.drawable.ic_warning) - : icon); + icon == null ? context.getDrawable(R.drawable.ic_warning) : icon); if (mAttentionLevel != AttentionLevel.NORMAL && !SettingsThemeHelper.isExpressiveTheme(context)) { iconView.setColorFilter( @@ -224,14 +234,24 @@ public class BannerMessagePreference extends Preference implements GroupSectionD if (IS_AT_LEAST_S) { @ColorInt final int backgroundColor = - context.getResources().getColor( - mAttentionLevel.getBackgroundColorResId(), theme); - - @ColorInt final int btnBackgroundColor = - context.getResources().getColor(mAttentionLevel.getButtonBackgroundColorResId(), - theme); - ColorStateList strokeColor = context.getResources().getColorStateList( - mAttentionLevel.getButtonBackgroundColorResId(), theme); + resources.getColor(mAttentionLevel.getBackgroundColorResId(), theme); + + ColorStateList btnBackgroundColor = + resources.getColorStateList( + mAttentionLevel.getButtonBackgroundColorResId(), theme); + ColorStateList btnStrokeColor = + mAttentionLevel == AttentionLevel.NORMAL + ? resources.getColorStateList( + R.color.settingslib_banner_outline_button_stroke_normal, theme) + : btnBackgroundColor; + ColorStateList filledBtnTextColor = + resources.getColorStateList( + mAttentionLevel.getButtonContentColorResId(), theme); + ColorStateList outlineBtnTextColor = + mAttentionLevel == AttentionLevel.NORMAL + ? btnBackgroundColor + : resources.getColorStateList( + R.color.settingslib_banner_outline_button_content, theme); holder.setDividerAllowedAbove(false); holder.setDividerAllowedBelow(false); @@ -242,10 +262,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD mPositiveButtonInfo.mColor = accentColor; mNegativeButtonInfo.mColor = accentColor; - if (mAttentionLevel != AttentionLevel.NORMAL) { - mPositiveButtonInfo.mBackgroundColor = btnBackgroundColor; - mNegativeButtonInfo.mStrokeColor = strokeColor; - } + mPositiveButtonInfo.mBackgroundColor = btnBackgroundColor; + mPositiveButtonInfo.mTextColor = filledBtnTextColor; + mNegativeButtonInfo.mStrokeColor = btnStrokeColor; + mNegativeButtonInfo.mTextColor = outlineBtnTextColor; mDismissButtonInfo.mButton = (ImageButton) holder.findViewById(R.id.banner_dismiss_btn); mDismissButtonInfo.setUpButton(); @@ -261,8 +281,6 @@ public class BannerMessagePreference extends Preference implements GroupSectionD headerView.setText(mHeader); headerView.setVisibility(TextUtils.isEmpty(mHeader) ? View.GONE : View.VISIBLE); } - - } else { holder.setDividerAllowedAbove(true); holder.setDividerAllowedBelow(true); @@ -567,8 +585,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD private boolean mIsVisible = true; private boolean mIsEnabled = true; @ColorInt private int mColor; - @ColorInt private int mBackgroundColor; + @Nullable private ColorStateList mBackgroundColor; @Nullable private ColorStateList mStrokeColor; + @Nullable private ColorStateList mTextColor; void setUpButton() { if (mButton == null) { @@ -586,12 +605,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD if (IS_AT_LEAST_S) { if (btn != null && SettingsThemeHelper.isExpressiveTheme(btn.getContext())) { - if (mBackgroundColor != 0) { - btn.setBackgroundColor(mBackgroundColor); + if (mBackgroundColor != null) { + btn.setBackgroundTintList(mBackgroundColor); } if (mStrokeColor != null) { btn.setStrokeColor(mStrokeColor); } + if (mTextColor != null) { + btn.setTextColor(mTextColor); + } } else { mButton.setTextColor(mColor); } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt index 9f2210d852a9..058fe53f7201 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.layout.Placeable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed @@ -226,7 +227,12 @@ internal fun AlertDialogFlowRow( val childrenMainAxisSizes = IntArray(placeables.size) { j -> placeables[j].width + - if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 + if ((layoutDirection == LayoutDirection.Ltr && j < placeables.lastIndex) + || (layoutDirection == LayoutDirection.Rtl && j > 0)) { + mainAxisSpacing.roundToPx() + } else { + 0 + } } val arrangement = Arrangement.End val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } diff --git a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt index e6c6638f7de4..c62aed1da352 100644 --- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt +++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt @@ -49,6 +49,7 @@ class StatusBannerPreference @JvmOverloads constructor( var iconLevel: BannerStatus = BannerStatus.GENERIC set(value) { field = value + updateIconTint(value) notifyChanged() } var buttonLevel: BannerStatus = BannerStatus.GENERIC @@ -81,7 +82,7 @@ class StatusBannerPreference @JvmOverloads constructor( if (icon == null) { icon = getIconDrawable(iconLevel) } else { - icon!!.setTintList(ColorStateList.valueOf(getBackgroundColor(iconLevel))) + updateIconTint(iconLevel) } buttonLevel = getInteger(R.styleable.StatusBanner_buttonLevel, 0).toBannerStatus() buttonText = getString(R.styleable.StatusBanner_buttonText) ?: "" @@ -252,4 +253,12 @@ class StatusBannerPreference @JvmOverloads constructor( ) } } -}
\ No newline at end of file + + /** + * Sets the icon's tint color based on the icon level. If an icon is not defined, this is a + * no-op. + */ + private fun updateIconTint(iconLevel: BannerStatus) { + icon?.setTintList(ColorStateList.valueOf(getBackgroundColor(iconLevel))) + } +} diff --git a/packages/SettingsLib/res/xml/timezones.xml b/packages/SettingsLib/res/xml/timezones.xml index 6a8d7802f9fd..4cea32aa05f9 100644 --- a/packages/SettingsLib/res/xml/timezones.xml +++ b/packages/SettingsLib/res/xml/timezones.xml @@ -35,6 +35,7 @@ <timezone id="Europe/Brussels"></timezone> <timezone id="Europe/Madrid"></timezone> <timezone id="Europe/Sarajevo"></timezone> + <timezone id="Europe/Warsaw"></timezone> <timezone id="Africa/Windhoek"></timezone> <timezone id="Africa/Brazzaville"></timezone> <timezone id="Asia/Amman"></timezone> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 4de64769b425..f89bd9c43a37 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -77,6 +77,10 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { private static final String ROLE_DEVICE_LOCK_CONTROLLER = "android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER"; + //TODO(b/378931989): Switch to android.app.admin.DevicePolicyIdentifiers.MEMORY_TAGGING_POLICY + //when the appropriate flag is launched. + private static final String MEMORY_TAGGING_POLICY = "memoryTagging"; + /** * @return drawables for displaying with settings that are locked by a device admin. */ @@ -244,14 +248,23 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { */ public static EnforcedAdmin checkIfKeyguardFeaturesDisabled(Context context, int keyguardFeatures, final @UserIdInt int userId) { - final LockSettingCheck check = (dpm, admin, checkUser) -> { - int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser); - if (checkUser != userId) { - effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; - } - return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE; - }; - if (UserManager.get(context).getUserInfo(userId).isManagedProfile()) { + UserInfo userInfo = UserManager.get(context).getUserInfo(userId); + if (userInfo == null) { + Log.w(LOG_TAG, "User " + userId + " does not exist"); + return null; + } + + final LockSettingCheck check = + (dpm, admin, checkUser) -> { + int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser); + if (checkUser != userId) { + // This case is reached when {@code checkUser} is a managed profile and + // {@code userId} is the parent user. + effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; + } + return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE; + }; + if (userInfo.isManagedProfile()) { DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); return findEnforcedAdmin(dpm.getActiveAdminsAsUser(userId), dpm, userId, check); @@ -838,14 +851,13 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { if (dpm.getMtePolicy() == MTE_NOT_CONTROLLED_BY_POLICY) { return null; } - EnforcedAdmin admin = - RestrictedLockUtils.getProfileOrDeviceOwner( - context, context.getUser()); - if (admin != null) { - return admin; + EnforcingAdmin enforcingAdmin = context.getSystemService(DevicePolicyManager.class) + .getEnforcingAdmin(context.getUserId(), MEMORY_TAGGING_POLICY); + if (enforcingAdmin == null) { + Log.w(LOG_TAG, "MTE is controlled by policy but could not find enforcing admin."); } - int profileId = getManagedProfileId(context, context.getUserId()); - return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId)); + + return EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(MEMORY_TAGGING_POLICY); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/display/OWNERS b/packages/SettingsLib/src/com/android/settingslib/display/OWNERS new file mode 100644 index 000000000000..aafc2f79611b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/display/OWNERS @@ -0,0 +1,5 @@ +# Default reviewers for this and subdirectories. +pbdr@google.com +ebrukurnaz@google.com +lihongyu@google.com +wilczynskip@google.com diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt index 4d38f1d551bb..cca43b92ef19 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt @@ -510,9 +510,9 @@ open class WifiUtils { AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP, AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION) intent.putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) - onStartActivity(intent) + withContext(Dispatchers.Main) { onStartActivity(intent) } } else if (wifiManager.isWepSupported == true && wifiManager.queryWepAllowed()) { - onAllowed() + withContext(Dispatchers.Main) { onAllowed() } } else { val intent = Intent(Intent.ACTION_MAIN).apply { component = ComponentName( @@ -522,7 +522,7 @@ open class WifiUtils { putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) putExtra(SSID, ssid) }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - onStartActivity(intent) + withContext(Dispatchers.Main) { onStartActivity(intent) } } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java index 785bcbf5a91c..71e11ba55850 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedLockUtilsTest.java @@ -458,6 +458,16 @@ public class RestrictedLockUtilsTest { assertThat(intentCaptor.getValue().getExtra(EXTRA_RESTRICTION)).isNull(); } + /** See b/386971405. Ensure that the code does not crash when the user is not found. */ + @Test + public void checkIfKeyguardFeaturesDisabled_returnsNull_whenUserDoesNotExist() { + when(mUserManager.getUserInfo(mUserId)).thenReturn(null); + assertThat( + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, KEYGUARD_DISABLE_FINGERPRINT, mUserId)) + .isNull(); + } + private UserInfo setUpUser(int userId, ComponentName[] admins) { UserInfo userInfo = new UserInfo(userId, "primary", 0); when(mUserManager.getUserInfo(userId)).thenReturn(userInfo); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java index 7f4bdaeac855..83471ae9513e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java @@ -18,6 +18,7 @@ package com.android.settingslib.widget; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.robolectric.Robolectric.setupActivity; @@ -25,6 +26,8 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.graphics.ColorFilter; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; @@ -38,24 +41,34 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; import androidx.preference.PreferenceViewHolder; -import com.android.settingslib.testutils.OverpoweredReflectionHelper; import com.android.settingslib.widget.preference.banner.R; +import com.google.android.material.button.MaterialButton; + import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowDrawable; import org.robolectric.shadows.ShadowTouchDelegate; import org.robolectric.util.ReflectionHelpers; -@Ignore("b/359066481") +import java.util.List; + @RunWith(RobolectricTestRunner.class) +@Config(shadows = {BannerMessagePreferenceTest.ShadowSettingsThemeHelper.class}) public class BannerMessagePreferenceTest { private Context mContext; @@ -66,14 +79,23 @@ public class BannerMessagePreferenceTest { private boolean mClickListenerCalled = false; private final View.OnClickListener mClickListener = v -> mClickListenerCalled = true; private final int mMinimumTargetSize = - RuntimeEnvironment.application.getResources() - .getDimensionPixelSize(com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target); + RuntimeEnvironment.application + .getResources() + .getDimensionPixelSize( + com.android.settingslib.widget.theme.R.dimen + .settingslib_preferred_minimum_touch_target); - private static final int TEST_STRING_RES_ID = - R.string.accessibility_banner_message_dismiss; + private static final int TEST_STRING_RES_ID = R.string.accessibility_banner_message_dismiss; + + @Mock private View mMockBackgroundView; + @Mock private Drawable mMockCardBackground; + @Mock private MaterialButton mMockPositiveBtn; + @Mock private MaterialButton mMockNegativeBtn; @Before public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowSettingsThemeHelper.setExpressiveTheme(false); mContext = RuntimeEnvironment.application; mClickListenerCalled = false; mBannerPreference = new BannerMessagePreference(mContext); @@ -90,6 +112,7 @@ public class BannerMessagePreferenceTest { .isEqualTo("test"); } + @Ignore("b/359066481") @Test public void onBindViewHolder_andOnLayoutView_dismissButtonTouchDelegate_isCorrectSize() { assumeAndroidS(); @@ -155,9 +178,8 @@ public class BannerMessagePreferenceTest { @Test public void onBindViewHolder_whenAtLeastS_whenSubtitleXmlAttribute_shouldSetSubtitle() { assumeAndroidS(); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.subtitle, "Test") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet().addAttribute(R.attr.subtitle, "Test").build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -185,8 +207,7 @@ public class BannerMessagePreferenceTest { ImageView mIcon = mRootView.findViewById(R.id.banner_icon); ShadowDrawable shadowDrawable = shadowOf(mIcon.getDrawable()); - assertThat(shadowDrawable.getCreatedFromResId()) - .isEqualTo(R.drawable.settingslib_ic_cross); + assertThat(shadowDrawable.getCreatedFromResId()).isEqualTo(R.drawable.settingslib_ic_cross); } @Test @@ -207,6 +228,7 @@ public class BannerMessagePreferenceTest { Button mPositiveButton = mRootView.findViewById(R.id.banner_positive_btn); assertThat(mPositiveButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mPositiveButton.getText()).isEqualTo(mContext.getString(TEST_STRING_RES_ID)); } @@ -218,6 +240,7 @@ public class BannerMessagePreferenceTest { Button mNegativeButton = mRootView.findViewById(R.id.banner_negative_btn); assertThat(mNegativeButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mNegativeButton.getText()).isEqualTo(mContext.getString(TEST_STRING_RES_ID)); } @@ -359,8 +382,6 @@ public class BannerMessagePreferenceTest { @Test public void onBindViewHolder_whenAtLeastS_whenAttentionUnset_setsHighTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.onBindViewHolder(mHolder); @@ -370,17 +391,15 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_high)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_high)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high)); } @Test public void onBindViewHolder_whenAtLeastS_whenAttentionHighByXML_setsHighTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.attentionLevel, "high") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet().addAttribute(R.attr.attentionLevel, "high").build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -391,17 +410,17 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_high)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_high)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high)); } @Test public void onBindViewHolder_whenAtLeastS_whenAttentionMediumByXML_setsMediumTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.attentionLevel, "medium") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet() + .addAttribute(R.attr.attentionLevel, "medium") + .build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -412,17 +431,15 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_medium)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_medium)); } @Test public void onBindViewHolder_whenAtLeastS_whenAttentionLowByXML_setsLowTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); - AttributeSet mAttributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.attentionLevel, "low") - .build(); + AttributeSet mAttributeSet = + Robolectric.buildAttributeSet().addAttribute(R.attr.attentionLevel, "low").build(); mBannerPreference = new BannerMessagePreference(mContext, mAttributeSet); mBannerPreference.onBindViewHolder(mHolder); @@ -433,14 +450,13 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_low)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_low)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_low)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_low)); } @Test public void setAttentionLevel_whenAtLeastS_whenHighAttention_setsHighTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.HIGH); mBannerPreference.onBindViewHolder(mHolder); @@ -451,14 +467,44 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_high)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_high)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_high)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_high)); } @Test - public void setAttentionLevel_whenAtLeastS_whenMedAttention_setsMediumTheme() { + public void setAttentionLevel_whenAtLeastS_whenHighAttentionAndExpressiveTheme_setsBtnTheme() { + setExpressiveTheme(true); + assumeAndroidS(); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue(); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue(); + doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn); + doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isTrue(); + mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.HIGH); + final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class); + ColorStateList filledBtnBackground = + getColorStateList(R.color.settingslib_banner_button_background_high); + ColorStateList filledBtnTextColor = + getColorStateList(R.color.settingslib_banner_filled_button_content_high); + ColorStateList outlineBtnTextColor = + getColorStateList(R.color.settingslib_banner_outline_button_content); + + mBannerPreference.onBindViewHolder(mHolder); + + verify(mMockPositiveBtn).setBackgroundTintList(captor.capture()); + verify(mMockPositiveBtn).setTextColor(captor.capture()); + verify(mMockNegativeBtn).setStrokeColor(captor.capture()); + verify(mMockNegativeBtn).setTextColor(captor.capture()); + List<ColorStateList> colors = captor.getAllValues(); + assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors()); + assertThat(colors.get(2).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(3).getColors()).isEqualTo(outlineBtnTextColor.getColors()); + } + + @Test + public void setAttentionLevel_whenAtLeastS_whenMedAttention_setsBtnMediumTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.MEDIUM); mBannerPreference.onBindViewHolder(mHolder); @@ -469,14 +515,42 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_medium)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_medium)); + + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_medium)); + } + + @Test + public void setAttentionLevel_whenAtLeastS_whenMedAttentionAndExpressiveTheme_setsBtnTheme() { + setExpressiveTheme(true); + mContext.getResources().getConfiguration().uiMode = Configuration.UI_MODE_NIGHT_NO; + assumeAndroidS(); + doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn); + doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn); + mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.MEDIUM); + final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class); + ColorStateList filledBtnBackground = + getColorStateList(R.color.settingslib_banner_button_background_medium); + ColorStateList filledBtnTextColor = + getColorStateList(R.color.settingslib_banner_filled_button_content_medium); + ColorStateList outlineBtnTextColor = + getColorStateList(R.color.settingslib_banner_outline_button_content); + + mBannerPreference.onBindViewHolder(mHolder); + + verify(mMockPositiveBtn).setBackgroundTintList(captor.capture()); + verify(mMockPositiveBtn).setTextColor(captor.capture()); + verify(mMockNegativeBtn).setStrokeColor(captor.capture()); + verify(mMockNegativeBtn).setTextColor(captor.capture()); + List<ColorStateList> colors = captor.getAllValues(); + assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors()); + assertThat(colors.get(2).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(3).getColors()).isEqualTo(outlineBtnTextColor.getColors()); } @Test public void setAttentionLevel_whenAtLeastS_whenLowAttention_setsLowTheme() { assumeAndroidS(); - Drawable mCardBackgroundSpy = spy(mRootView.getBackground()); - mRootView.setBackground(mCardBackgroundSpy); mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.LOW); mBannerPreference.onBindViewHolder(mHolder); @@ -487,7 +561,37 @@ public class BannerMessagePreferenceTest { .isEqualTo(getColorId(R.color.banner_accent_attention_low)); assertThat(getButtonColor(R.id.banner_negative_btn)) .isEqualTo(getColorId(R.color.banner_accent_attention_low)); - verify(mCardBackgroundSpy).setTint(getColorId(R.color.banner_background_attention_low)); + verify(mMockCardBackground).setTint(getColorId(R.color.banner_background_attention_low)); + } + + @Test + public void + setAttentionLevel_whenAtLeastS_whenNormalAttentionAndExpressiveTheme_setsBtnTheme() { + setExpressiveTheme(true); + mContext.getResources().getConfiguration().uiMode = Configuration.UI_MODE_NIGHT_NO; + assumeAndroidS(); + doReturn(mMockPositiveBtn).when(mHolder).findViewById(R.id.banner_positive_btn); + doReturn(mMockNegativeBtn).when(mHolder).findViewById(R.id.banner_negative_btn); + mBannerPreference.setAttentionLevel(BannerMessagePreference.AttentionLevel.NORMAL); + final ArgumentCaptor<ColorStateList> captor = ArgumentCaptor.forClass(ColorStateList.class); + ColorStateList filledBtnBackground = + getColorStateList(R.color.settingslib_banner_button_background_normal); + ColorStateList filledBtnTextColor = + getColorStateList(R.color.settingslib_banner_filled_button_content_normal); + ColorStateList outlineBtnStrokeColor = + getColorStateList(R.color.settingslib_banner_outline_button_stroke_normal); + + mBannerPreference.onBindViewHolder(mHolder); + + verify(mMockPositiveBtn).setBackgroundTintList(captor.capture()); + verify(mMockPositiveBtn).setTextColor(captor.capture()); + verify(mMockNegativeBtn).setStrokeColor(captor.capture()); + verify(mMockNegativeBtn).setTextColor(captor.capture()); + List<ColorStateList> colors = captor.getAllValues(); + assertThat(colors.get(0).getColors()).isEqualTo(filledBtnBackground.getColors()); + assertThat(colors.get(1).getColors()).isEqualTo(filledBtnTextColor.getColors()); + assertThat(colors.get(2).getColors()).isEqualTo(outlineBtnStrokeColor.getColors()); + assertThat(colors.get(3).getColors()).isEqualTo(filledBtnBackground.getColors()); } private int getButtonColor(int buttonResId) { @@ -495,6 +599,11 @@ public class BannerMessagePreferenceTest { return mButton.getTextColors().getDefaultColor(); } + private ColorStateList getButtonTextColor(int buttonResId) { + Button mButton = mRootView.findViewById(buttonResId); + return mButton.getTextColors(); + } + private ColorFilter getColorFilter(@ColorRes int colorResId) { return new PorterDuffColorFilter(getColorId(colorResId), PorterDuff.Mode.SRC_IN); } @@ -503,28 +612,57 @@ public class BannerMessagePreferenceTest { return mContext.getResources().getColor(colorResId, mContext.getTheme()); } + private ColorStateList getColorStateList(@ColorRes int colorResId) { + return mContext.getResources().getColorStateList(colorResId, mContext.getTheme()); + } + private void assumeAndroidR() { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 30); ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "R"); - OverpoweredReflectionHelper - .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false); - // Reset view holder to use correct layout. - } - + // Refresh the static final field IS_AT_LEAST_S + mBannerPreference = new BannerMessagePreference(mContext); + setUpViewHolder(); + } private void assumeAndroidS() { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 31); ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "S"); - OverpoweredReflectionHelper - .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true); - // Re-inflate view to update layout. + + // Refresh the static final field IS_AT_LEAST_S + mBannerPreference = new BannerMessagePreference(mContext); setUpViewHolder(); } + private void setExpressiveTheme(boolean isExpressiveTheme) { + ShadowSettingsThemeHelper.setExpressiveTheme(isExpressiveTheme); + assertThat(SettingsThemeHelper.isExpressiveTheme(mContext)).isEqualTo(isExpressiveTheme); + if (isExpressiveTheme) { + doReturn(mContext).when(mMockPositiveBtn).getContext(); + doReturn(mContext).when(mMockNegativeBtn).getContext(); + } + } + private void setUpViewHolder() { mRootView = View.inflate(mContext, mBannerPreference.getLayoutResource(), null /* parent */); - mHolder = PreferenceViewHolder.createInstanceForTests(mRootView); + mHolder = spy(PreferenceViewHolder.createInstanceForTests(mRootView)); + doReturn(mMockBackgroundView).when(mHolder).findViewById(R.id.banner_background); + doReturn(mMockCardBackground).when(mMockBackgroundView).getBackground(); + } + + @Implements(SettingsThemeHelper.class) + public static class ShadowSettingsThemeHelper { + private static boolean sIsExpressiveTheme; + + /** Shadow implementation of isExpressiveTheme */ + @Implementation + public static boolean isExpressiveTheme(@NonNull Context context) { + return sIsExpressiveTheme; + } + + static void setExpressiveTheme(boolean isExpressiveTheme) { + sIsExpressiveTheme = isExpressiveTheme; + } } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java index d8b6707b9118..97473fffaeb1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java @@ -50,6 +50,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -211,10 +212,12 @@ public class WifiUtilsTest { WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext); for (int level = 0; level <= 4; level++) { + Mockito.reset(mContext); iconInjector.getIcon(false /* noInternet */, level); verify(mContext).getDrawable( WifiUtils.getInternetIconResource(level, false /* noInternet */)); + Mockito.reset(mContext); iconInjector.getIcon(true /* noInternet */, level); verify(mContext).getDrawable( WifiUtils.getInternetIconResource(level, true /* noInternet */)); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index a178869d23d6..ea09d634b82b 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -961,6 +961,7 @@ android:featureFlag="android.security.aapm_api"/> <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" android:featureFlag="android.security.aapm_api"/> + <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_MTE" /> <!-- Permission required for CTS test - IntrusionDetectionManagerTest --> <uses-permission android:name="android.permission.READ_INTRUSION_DETECTION_STATE" diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index ab3fa1b06255..728d206e2786 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -97,7 +97,6 @@ spdonghao@google.com steell@google.com stevenckng@google.com stwu@google.com -syeonlee@google.com sunnygoyal@google.com thiruram@google.com tracyzhou@google.com diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 17d8bbfd2240..91492b2959d8 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -512,13 +512,6 @@ flag { } flag { - name: "status_bar_notification_chips" - namespace: "systemui" - description: "Show promoted ongoing notifications as chips in the status bar" - bug: "364653005" -} - -flag { name: "status_bar_popup_chips" namespace: "systemui" description: "Show rich ongoing processes as chips in the status bar" @@ -2038,13 +2031,6 @@ flag { } flag { - name: "ui_rich_ongoing_force_expanded" - namespace: "systemui" - description: "Force promoted notifications to always be expanded" - bug: "380901479" -} - -flag { name: "permission_helper_ui_rich_ongoing" namespace: "systemui" description: "[RONs] Guards inline permission helper for demoting RONs [Guts/card version]" @@ -2059,13 +2045,6 @@ flag { } flag { - name: "aod_ui_rich_ongoing" - namespace: "systemui" - description: "Show a rich ongoing notification on the always-on display (depends on ui_rich_ongoing)" - bug: "369151941" -} - -flag { name: "stabilize_heads_up_group_v2" namespace: "systemui" description: "Stabilize heads up groups in VisualStabilityCoordinator" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 2b8fe39c4870..16a8f987ba83 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.ui.compose +import android.content.res.Configuration import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -26,6 +28,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.disabled @@ -55,6 +58,7 @@ import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffset import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource @@ -98,6 +102,17 @@ val sceneTransitionsV2 = transitions { spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) fade(AllElements) } + to(CommunalScenes.Blank, key = CommunalTransitionKeys.SwipeInLandscape) { + spec = tween(durationMillis = TO_LOCKSCREEN_DURATION.toInt(DurationUnit.MILLISECONDS)) + translate(Communal.Elements.Grid, Edge.End) + timestampRange(endMillis = 167) { + fade(Communal.Elements.Grid) + fade(Communal.Elements.IndicationArea) + fade(Communal.Elements.LockIcon) + fade(Communal.Elements.StatusBar) + } + timestampRange(startMillis = 167, endMillis = 500) { fade(Communal.Elements.Scrim) } + } to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) { spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) translate(Communal.Elements.Grid, Edge.End) @@ -214,6 +229,9 @@ fun CommunalContainer( val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() } + val swipeFromHubInLandscape by + viewModel.swipeFromHubInLandscape.collectAsStateWithLifecycle(false) + SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) }, @@ -241,7 +259,14 @@ fun CommunalContainer( userActions = mapOf( Swipe.End to - UserActionResult(CommunalScenes.Blank, CommunalTransitionKeys.Swipe) + UserActionResult( + CommunalScenes.Blank, + if (swipeFromHubInLandscape) { + CommunalTransitionKeys.SwipeInLandscape + } else { + CommunalTransitionKeys.Swipe + }, + ) ), ) { CommunalScene( @@ -258,6 +283,20 @@ fun CommunalContainer( Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed)) } +/** Listens to orientation changes on communal scene and reset when scene is disposed. */ +@Composable +fun ObserveOrientationChange(viewModel: CommunalViewModel) { + val configuration = LocalConfiguration.current + + LaunchedEffect(configuration.orientation) { + viewModel.onOrientationChange(configuration.orientation) + } + + DisposableEffect(Unit) { + onDispose { viewModel.onOrientationChange(Configuration.ORIENTATION_UNDEFINED) } + } +} + /** Scene containing the glanceable hub UI. */ @Composable fun ContentScope.CommunalScene( @@ -269,6 +308,8 @@ fun ContentScope.CommunalScene( ) { val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) + // Observe screen rotation while Communal Scene is active. + ObserveOrientationChange(viewModel) Box( modifier = Modifier.element(Communal.Elements.Scrim) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 7cd6c6b47f2a..37ce358a2a98 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -49,6 +49,7 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader +import com.android.systemui.shade.ui.composable.isFullWidthShade import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.util.Utils import dagger.Lazy @@ -117,7 +118,7 @@ constructor( ) { Box { Column { - if (viewModel.showClock) { + if (isFullWidthShade()) { val burnIn = rememberBurnIn(keyguardClockViewModel) with(clockSection) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt index 652a2ff21e9b..87eea82ef30d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt @@ -19,12 +19,17 @@ package com.android.systemui.clipboardoverlay import android.content.ClipData import android.content.ComponentName import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.net.Uri import android.text.SpannableString import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -33,6 +38,8 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -40,8 +47,10 @@ class ActionIntentCreatorTest : SysuiTestCase() { private val scheduler = TestCoroutineScheduler() private val mainDispatcher = UnconfinedTestDispatcher(scheduler) private val testScope = TestScope(mainDispatcher) + val packageManager = mock<PackageManager>() - val creator = ActionIntentCreator(testScope.backgroundScope) + val creator = + ActionIntentCreator(context, packageManager, testScope.backgroundScope, mainDispatcher) @Test fun test_getTextEditorIntent() { @@ -73,7 +82,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { } @Test - fun test_getImageEditIntent() = runTest { + fun test_getImageEditIntent_noDefault() = runTest { context.getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, "") val fakeUri = Uri.parse("content://foo") var intent = creator.getImageEditIntent(fakeUri, context) @@ -83,18 +92,82 @@ class ActionIntentCreatorTest : SysuiTestCase() { assertEquals(null, intent.component) assertEquals("clipboard", intent.getStringExtra("edit_source")) assertFlags(intent, EXTERNAL_INTENT_FLAGS) + } + + @Test + fun test_getImageEditIntent_defaultProvided() = runTest { + val fakeUri = Uri.parse("content://foo") - // try again with an editor component val fakeComponent = ComponentName("com.android.remotecopy", "com.android.remotecopy.RemoteCopyActivity") context .getOrCreateTestableResources() .addOverride(R.string.config_screenshotEditor, fakeComponent.flattenToString()) - intent = creator.getImageEditIntent(fakeUri, context) + val intent = creator.getImageEditIntent(fakeUri, context) assertEquals(fakeComponent, intent.component) } @Test + fun test_getImageEditIntent_preferredProvidedButDisabled() = runTest { + val fakeUri = Uri.parse("content://foo") + + val defaultComponent = ComponentName("com.android.foo", "com.android.foo.Something") + val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") + + val packageInfo = + PackageInfo().apply { + activities = arrayOf() // no activities + } + whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt())) + .thenReturn(packageInfo) + + context + .getOrCreateTestableResources() + .addOverride(R.string.config_screenshotEditor, defaultComponent.flattenToString()) + context + .getOrCreateTestableResources() + .addOverride( + R.string.config_preferredScreenshotEditor, + preferredComponent.flattenToString(), + ) + val intent = creator.getImageEditIntent(fakeUri, context) + assertEquals(defaultComponent, intent.component) + } + + @Test + fun test_getImageEditIntent_preferredProvided() = runTest { + val fakeUri = Uri.parse("content://foo") + + val defaultComponent = ComponentName("com.android.foo", "com.android.foo.Something") + val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") + + val packageInfo = + PackageInfo().apply { + activities = + arrayOf( + ActivityInfo().apply { + packageName = preferredComponent.packageName + name = preferredComponent.className + } + ) + } + whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt())) + .thenReturn(packageInfo) + + context + .getOrCreateTestableResources() + .addOverride(R.string.config_screenshotEditor, defaultComponent.flattenToString()) + context + .getOrCreateTestableResources() + .addOverride( + R.string.config_preferredScreenshotEditor, + preferredComponent.flattenToString(), + ) + val intent = creator.getImageEditIntent(fakeUri, context) + assertEquals(preferredComponent, intent.component) + } + + @Test fun test_getShareIntent_plaintext() { val clipData = ClipData.newPlainText("Test", "Test Item") val intent = creator.getShareIntent(clipData, context) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt index 856a62e3f5a7..a6be3ce43b6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.communal.domain.interactor +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.common.data.repository.batteryRepository import com.android.systemui.common.data.repository.fake @@ -47,6 +49,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_GLANCEABLE_HUB_V2) class CommunalAutoOpenInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() @@ -54,6 +57,7 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { @Before fun setUp() { + kosmos.setCommunalV2ConfigEnabled(true) runBlocking { kosmos.fakeUserRepository.asMainUser() } with(kosmos.fakeSettings) { putIntForUser( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt index dc21f0692c9e..7bdac476641b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.communal.domain.interactor +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization @@ -33,11 +35,15 @@ import com.android.systemui.flags.andSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.scene.initialSceneKey import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos 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.advanceTimeBy +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -46,9 +52,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -70,6 +78,7 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase( private val repository = kosmos.communalSceneRepository private val underTest by lazy { kosmos.communalSceneInteractor } + private val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController @DisableFlags(FLAG_SCENE_CONTAINER) @Test @@ -551,4 +560,57 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase( transitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen) assertThat(isCommunalVisible).isEqualTo(false) } + + @Test + fun willRotateToPortrait_whenKeyguardRotationNotAllowed() = + testScope.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(false) + val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(true) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + } + + @Test + fun willRotateToPortrait_isFalse_whenKeyguardRotationIsAllowed() = + testScope.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(true) + val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + } + + @Test + fun rotatedToPortrait() = + testScope.runTest { + val rotatedToPortrait by collectLastValue(underTest.rotatedToPortrait) + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(true) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt index d6f7145bd770..c671aed1f4a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt @@ -21,9 +21,11 @@ import android.app.admin.devicePolicyManager import android.content.Intent import android.content.pm.UserInfo import android.os.UserManager +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.shared.model.WhenToStartHub @@ -86,8 +88,10 @@ class CommunalSettingsInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun whenToStartHub_matchesRepository() = kosmos.runTest { + setCommunalV2ConfigEnabled(true) fakeSettings.putIntForUser( Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB, Settings.Secure.GLANCEABLE_HUB_START_CHARGING, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt index b4708d97c4c3..80f0005cb73f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt @@ -53,7 +53,7 @@ class PosturingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val underTest by lazy { kosmos.posturingInteractor } + private val Kosmos.underTest by Kosmos.Fixture { kosmos.posturingInteractor } @Test fun testNoDebugOverride() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt new file mode 100644 index 000000000000..581f3cb172fe --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt @@ -0,0 +1,74 @@ +/* + * 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.communal.util + +import android.testing.AndroidTestingRunner +import android.view.MotionEvent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class UserTouchActivityNotifierTest : SysuiTestCase() { + private val kosmos: Kosmos = testKosmos().useUnconfinedTestDispatcher() + + @Test + fun firstEventTriggersNotify() = + kosmos.runTest { sendEventAndVerify(0, MotionEvent.ACTION_MOVE, true) } + + @Test + fun secondEventTriggersRateLimited() = + kosmos.runTest { + var eventTime = 0L + + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) + eventTime += 50 + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, false) + eventTime += USER_TOUCH_ACTIVITY_RATE_LIMIT + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) + } + + @Test + fun overridingActionNotifies() = + kosmos.runTest { + var eventTime = 0L + sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) + sendEventAndVerify(eventTime, MotionEvent.ACTION_DOWN, true) + sendEventAndVerify(eventTime, MotionEvent.ACTION_UP, true) + sendEventAndVerify(eventTime, MotionEvent.ACTION_CANCEL, true) + } + + private fun sendEventAndVerify(eventTime: Long, action: Int, shouldBeHandled: Boolean) { + kosmos.fakePowerRepository.userTouchRegistered = false + val motionEvent = MotionEvent.obtain(0, eventTime, action, 0f, 0f, 0) + kosmos.userTouchActivityNotifier.notifyActivity(motionEvent) + + if (shouldBeHandled) { + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() + } else { + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() + } + } +} 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 d2d8ab9d5cb7..e6153e8ab337 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 @@ -20,6 +20,8 @@ import android.content.pm.UserInfo import android.hardware.biometrics.BiometricFaceConstants import android.hardware.biometrics.BiometricSourceType import android.os.PowerManager +import android.platform.test.annotations.EnableFlags +import android.service.dreams.Flags.FLAG_DREAMS_V2 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState @@ -157,6 +159,33 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_DREAMS_V2) + fun faceAuthIsRequestedWhenTransitioningFromDreamToLockscreen() = + testScope.runTest { + underTest.start() + runCurrent() + + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) + faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( + setOf(WakeSleepReason.LID.powerManagerWakeReason) + ) + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.DREAMING, + KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + ) + ) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true) + ) + } + + @Test fun whenFaceIsLockedOutAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() = testScope.runTest { underTest.start() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt index 90500839c8ad..a7810a69265a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt @@ -16,13 +16,17 @@ package com.android.systemui.deviceentry.domain.ui.viewmodel +import android.graphics.Point import android.platform.test.flag.junit.FlagsParameterization +import android.view.MotionEvent +import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.udfpsUtils import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.data.ui.viewmodel.alternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.data.ui.viewmodel.deviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER @@ -34,6 +38,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.accessibilityActionsViewModelKosmos import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition import com.android.systemui.kosmos.testScope import com.android.systemui.res.R @@ -41,14 +46,22 @@ import com.android.systemui.shade.shadeTestUtil 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.After import org.junit.Before import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -63,7 +76,6 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository - private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository private val shadeTestUtil by lazy { kosmos.shadeTestUtil } @@ -83,6 +95,22 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys @Before fun setup() { + whenever(kosmos.udfpsUtils.isWithinSensorArea(any(), any(), any())).thenReturn(false) + whenever( + kosmos.udfpsUtils.getTouchInNativeCoordinates(anyInt(), any(), any(), anyBoolean()) + ) + .thenReturn(Point(0, 0)) + whenever( + kosmos.udfpsUtils.onTouchOutsideOfSensorArea( + anyBoolean(), + eq(null), + anyInt(), + anyInt(), + any(), + anyBoolean(), + ) + ) + .thenReturn("Move left") underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel overrideResource(R.integer.udfps_padding_debounce_duration, 0) } @@ -101,6 +129,55 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys } @Test + fun contentDescription_setOnUdfpsTouchOutsideSensorArea() = + testScope.runTest { + val contentDescription by collectLastValue(underTest.contentDescription) + setupVisibleStateOnLockscreen() + underTest.onHoverEvent(mock<View>(), mock<MotionEvent>()) + runCurrent() + assertThat(contentDescription).isEqualTo("Move left") + } + + @Test + fun clearAccessibilityOverlayMessageReason_updatesWhenFocusChangesFromUdfpsOverlayToLockscreen() = + testScope.runTest { + val clearAccessibilityOverlayMessageReason by + collectLastValue(underTest.clearAccessibilityOverlayMessageReason) + val contentDescription by collectLastValue(underTest.contentDescription) + setupVisibleStateOnLockscreen() + kosmos.accessibilityActionsViewModelKosmos.clearUdfpsAccessibilityOverlayMessage("test") + runCurrent() + assertThat(clearAccessibilityOverlayMessageReason).isEqualTo("test") + + // UdfpsAccessibilityOverlayViewBinder collects clearAccessibilityOverlayMessageReason + // and calls + // viewModel.setContentDescription(null) - mock this here + underTest.setContentDescription(null) + runCurrent() + assertThat(contentDescription).isNull() + } + + @Test + fun clearAccessibilityOverlayMessageReason_updatesAfterUdfpsOverlayFocusOnAlternateBouncer() = + testScope.runTest { + val clearAccessibilityOverlayMessageReason by + collectLastValue(underTest.clearAccessibilityOverlayMessageReason) + val contentDescription by collectLastValue(underTest.contentDescription) + setupVisibleStateOnLockscreen() + kosmos.alternateBouncerUdfpsAccessibilityOverlayViewModel + .clearUdfpsAccessibilityOverlayMessage("test") + runCurrent() + assertThat(clearAccessibilityOverlayMessageReason).isEqualTo("test") + + // UdfpsAccessibilityOverlayViewBinder collects clearAccessibilityOverlayMessageReason + // and calls + // viewModel.setContentDescription(null) - mock this here + underTest.setContentDescription(null) + runCurrent() + assertThat(contentDescription).isNull() + } + + @Test fun touchExplorationNotEnabled_overlayNotVisible() = testScope.runTest { val visible by collectLastValue(underTest.visible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index af6c65ec6d6d..1f74ad496bbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -533,7 +533,7 @@ object TestShortcuts { val expectedShortcutCategoriesWithSimpleShortcutCombination = listOf( - simpleShortcutCategory(System, "System apps", "Open assistant"), + simpleShortcutCategory(System, "System apps", "Open digital assistant"), simpleShortcutCategory(System, "System controls", "Go to home screen"), simpleShortcutCategory(System, "System apps", "Open settings"), simpleShortcutCategory(System, "System controls", "Lock screen"), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt new file mode 100644 index 000000000000..c515fc394bda --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModelTest.kt @@ -0,0 +1,63 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DreamingToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest by lazy { kosmos.dreamingToPrimaryBouncerViewModel } + + @Test + fun dreamingToPrimaryBouncerChangesBlurToMax() = + testScope.runTest { + val values by collectValues(underTest.windowBlurRadius) + + kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0.0f, 0.0f, 0.3f, 0.4f, 0.5f, 1.0f), + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.maxBlurRadiusPx, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + private fun step(value: Float, transitionState: TransitionState = RUNNING) = + TransitionStep( + from = KeyguardState.DREAMING, + to = KeyguardState.PRIMARY_BOUNCER, + value = value, + transitionState = transitionState, + ownerName = "dreamingToPrimaryBouncerTransitionViewModelTest", + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt index 3ab920a46084..cdd093a410df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt @@ -17,11 +17,20 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Configuration +import android.content.res.mainResources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.util.LayoutDirection -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -29,30 +38,53 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.transitions.blurConfig import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { - val kosmos = testKosmos() +@RunWith(ParameterizedAndroidJunit4::class) +class GlanceableHubToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : + SysuiTestCase() { + val kosmos = testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } val testScope = kosmos.testScope val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val configurationRepository = kosmos.fakeConfigurationRepository + val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel } + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Test fun lockscreenFadeIn() = kosmos.runTest { + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardAlpha) assertThat(values).isEmpty() @@ -79,6 +111,116 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenFadeIn_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).isEmpty() + + // Exit hub to lockscreen + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .2f + + // Still in landscape + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + // start here.. + step(0.5f), + ), + testScope, + ) + + // Communal container is rotated to portrait + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0.6f), + step(0.7f), + // should stop here.. + step(0.8f), + step(1f), + ), + testScope, + ) + // Scene transition finished. + progress.value = 1f + keyguardTransitionRepository.sendTransitionSteps( + listOf(step(1f, TransitionState.FINISHED)), + testScope, + ) + + assertThat(values).hasSize(4) + // onStart + assertThat(values[0]).isEqualTo(0f) + assertThat(values[1]).isEqualTo(0f) + assertThat(values[2]).isEqualTo(1f) + // onFinish + assertThat(values[3]).isEqualTo(1f) + } + + @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenFadeIn_v2FlagDisabledAndFromHubInLandscape() = + kosmos.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + // Rotation is not enabled so communal container is in portrait. + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).isEmpty() + + // Exit hub to lockscreen + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + // Should start running here... + step(0.1f), + step(0.2f), + step(0.3f), + step(0.4f), + // ...up to here + step(0.5f), + step(0.6f), + step(0.7f), + step(0.8f), + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + } + + @Test fun lockscreenTranslationX() = kosmos.runTest { val config: Configuration = mock() @@ -89,6 +231,8 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100, ) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() @@ -108,6 +252,44 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenTranslationX_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100, + ) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.5f), + step(0.7f), + step(1f), + step(1f, TransitionState.FINISHED), + ), + testScope, + ) + // no translation-x animation + values.forEach { assertThat(it.value).isEqualTo(0f) } + } + + @Test fun lockscreenTranslationX_resetsAfterCancellation() = kosmos.runTest { val config: Configuration = mock() @@ -118,6 +300,9 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100, ) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() @@ -137,6 +322,42 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenTranslationX_resetsAfterCancellation_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100, + ) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.6f), + step(0.9f, TransitionState.CANCELED), + ), + testScope, + ) + // no translation-x animation + values.forEach { assertThat(it.value).isEqualTo(0f) } + } + + @Test @DisableSceneContainer fun blurBecomesMinValueImmediately() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index fe213a6ebbf0..71e09d982494 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -17,12 +17,19 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.content.res.mainResources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.view.View import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository @@ -35,6 +42,9 @@ import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.sceneContainerRepository @@ -48,6 +58,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -69,7 +80,8 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = + testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } private val testScope = kosmos.testScope private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } @@ -419,6 +431,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) @@ -439,6 +452,84 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableSceneContainer + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun alpha_transitionFromHubToLockscreenInLandscape_isOne() = + kosmos.runTest { + setCommunalV2ConfigEnabled(true) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val alpha by collectLastValue(underTest.alpha(viewState)) + + // Transition to the glanceable hub and back. + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope, + ) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() + + // Exit hub to lockscreen + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .4f + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + value = 0f, + ), + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.RUNNING, + value = 0.4f, + ), + ), + testScope, + ) + + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.FINISHED, + value = 1f, + ) + ), + testScope, + ) + + assertThat(alpha).isEqualTo(1.0f) + } + + @Test fun alpha_emitsOnShadeExpansion() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..9c2c3c3f1498 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelTest.kt @@ -0,0 +1,74 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PrimaryBouncerToDreamingTransitionViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var underTest: PrimaryBouncerToDreamingTransitionViewModel + + @Before + fun setUp() { + keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.primaryBouncerToDreamingTransitionViewModel + } + + @Test + fun blurRadiusGoesToMinImmediately() = + testScope.runTest { + val values by collectValues(underTest.windowBlurRadius) + + kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f), + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.minBlurRadiusPx, + actualValuesProvider = { values }, + transitionFactory = ::step, + ) + } + + private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep { + return TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.DREAMING, + value = value, + transitionState = state, + ownerName = "PrimaryBouncerToDreamingTransitionViewModelTest", + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt index 04ef1be9c057..ab605c0ea14e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt @@ -18,12 +18,17 @@ package com.android.systemui.mediaprojection.permission import android.app.AlertDialog import android.media.projection.MediaProjectionConfig +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.testing.TestableLooper import android.view.WindowManager import android.widget.Spinner import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.res.R @@ -32,6 +37,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.google.common.truth.Truth.assertThat import kotlin.test.assertEquals import org.junit.After +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -41,6 +47,8 @@ import org.mockito.kotlin.mock @TestableLooper.RunWithLooper(setAsMainLooper = true) class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { + @get:Rule val checkFlagRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private lateinit var dialog: AlertDialog private val appName = "Test App" @@ -51,6 +59,8 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { R.string.media_projection_entry_app_permission_dialog_option_text_entire_screen private val resIdSingleAppDisabled = R.string.media_projection_entry_app_permission_dialog_single_app_disabled + private val resIdSingleAppNotSupported = + R.string.media_projection_entry_app_permission_dialog_single_app_not_supported @After fun teardown() { @@ -78,6 +88,7 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) fun showDialog_disableSingleApp() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() @@ -98,10 +109,34 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) + fun showDialog_disableSingleApp_appNotSupported() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() + ) + + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + val secondOptionWarningText = + spinner.adapter + .getDropDownView(1, null, spinner) + .findViewById<TextView>(android.R.id.text2) + ?.text + + // check that the first option is full screen and enabled + assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) + + // check that the second option is single app and disabled + assertEquals( + context.getString(resIdSingleAppNotSupported, appName), + secondOptionWarningText, + ) + } + + @Test fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(), - overrideDisableSingleAppOption = true + overrideDisableSingleAppOption = true, ) val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) @@ -161,7 +196,7 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { appName, overrideDisableSingleAppOption, hostUid = 12345, - mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>() + mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>(), ) dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt index 6495b66cc148..17cdb8dd592d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt @@ -18,12 +18,17 @@ package com.android.systemui.mediaprojection.permission import android.app.AlertDialog import android.media.projection.MediaProjectionConfig +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.testing.TestableLooper import android.view.WindowManager import android.widget.Spinner import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.res.R @@ -32,6 +37,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.google.common.truth.Truth.assertThat import kotlin.test.assertEquals import org.junit.After +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -41,6 +47,8 @@ import org.mockito.kotlin.mock @TestableLooper.RunWithLooper(setAsMainLooper = true) class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { + @get:Rule val checkFlagRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private lateinit var dialog: AlertDialog private val appName = "Test App" @@ -51,6 +59,8 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { R.string.media_projection_entry_cast_permission_dialog_option_text_entire_screen private val resIdSingleAppDisabled = R.string.media_projection_entry_app_permission_dialog_single_app_disabled + private val resIdSingleAppNotSupported = + R.string.media_projection_entry_app_permission_dialog_single_app_not_supported @After fun teardown() { @@ -78,6 +88,7 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) fun showDialog_disableSingleApp() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() @@ -98,6 +109,30 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { } @Test + @RequiresFlagsEnabled(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT) + fun showDialog_disableSingleApp_appNotSupported() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() + ) + + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + val secondOptionWarningText = + spinner.adapter + .getDropDownView(1, null, spinner) + .findViewById<TextView>(android.R.id.text2) + ?.text + + // check that the first option is full screen and enabled + assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) + + // check that the second option is single app and disabled + assertEquals( + context.getString(resIdSingleAppNotSupported, appName), + secondOptionWarningText, + ) + } + + @Test fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() { setUpAndShowDialog( mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(), @@ -169,7 +204,7 @@ class SystemCastPermissionDialogDelegateTest : SysuiTestCase() { SystemUIDialog.setDialogSize(dialog) dialog.window?.addSystemFlags( - WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS ) delegate.onCreate(dialog, savedInstanceState = null) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index ffcd95bc7a4a..cd7b658518b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -38,13 +38,10 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository -import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository -import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -116,38 +113,6 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showClock_showsOnNarrowScreen() = - testScope.runTest { - kosmos.shadeRepository.setShadeLayoutWide(false) - - // Shown when notifications are present. - kosmos.activeNotificationListRepository.setActiveNotifs(1) - runCurrent() - assertThat(underTest.showClock).isTrue() - - // Hidden when notifications are not present. - kosmos.activeNotificationListRepository.setActiveNotifs(0) - runCurrent() - assertThat(underTest.showClock).isFalse() - } - - @Test - fun showClock_hidesOnWideScreen() = - testScope.runTest { - kosmos.shadeRepository.setShadeLayoutWide(true) - - // Hidden when notifications are present. - kosmos.activeNotificationListRepository.setActiveNotifs(1) - runCurrent() - assertThat(underTest.showClock).isFalse() - - // Hidden when notifications are not present. - kosmos.activeNotificationListRepository.setActiveNotifs(0) - runCurrent() - assertThat(underTest.showClock).isFalse() - } - - @Test fun showMedia_activeMedia_true() = testScope.runTest { kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index bfd700dcc302..52996ee1e369 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -309,24 +309,6 @@ public class NotificationEntryTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - public void isPromotedOngoing_uiFlagOnAndNotifHasFlag_true() { - mEntry.getSbn().getNotification().flags |= FLAG_PROMOTED_ONGOING; - - assertTrue(mEntry.isPromotedOngoing()); - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - public void isPromotedOngoing_statusBarNotifChipsFlagOnAndNotifHasFlag_true() { - mEntry.getSbn().getNotification().flags |= FLAG_PROMOTED_ONGOING; - - assertTrue(mEntry.isPromotedOngoing()); - } - - @Test public void testIsNotificationVisibilityPrivate_true() { assertTrue(mEntry.isNotificationVisibilityPrivate()); } 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 8560b66d961f..5b0e4e139d4e 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 @@ -749,20 +749,6 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(getIsSticky_promotedAndExpanded()).isFalse() } - @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun testIsSticky_promotedAndExpanded_promotedUiFlagOn_false() { - assertThat(getIsSticky_promotedAndExpanded()).isFalse() - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_false() { - assertThat(getIsSticky_promotedAndExpanded()).isFalse() - } - private fun getIsSticky_promotedAndExpanded(): Boolean { val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() notif.flags = FLAG_PROMOTED_ONGOING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt index 4cfc7f98e5e7..df77b5ad46e8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt @@ -76,26 +76,6 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun shouldExtract_promotedNotificationUiFlagEnabled() = - kosmos.runTest { - val entry = createEntry() - val content = extractContent(entry) - assertThat(content).isNotNull() - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - fun shouldExtract_statusBarNotifChipsFlagEnabled() = - kosmos.runTest { - val entry = createEntry() - val content = extractContent(entry) - assertThat(content).isNotNull() - } - - @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun shouldExtract_bothFlagsEnabled() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt new file mode 100644 index 000000000000..bad33a402ff7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationsInteractorTest.kt @@ -0,0 +1,147 @@ +/* + * 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 android.app.Notification +import android.content.applicationContext +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.dump.dumpManager +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +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.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.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.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor +import com.android.systemui.statusbar.policy.mockSensitiveNotificationProtectionController +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 +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, +) +class AODPromotedNotificationsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Fixture { + AODPromotedNotificationInteractor( + promotedNotificationsInteractor = promotedNotificationsInteractor, + keyguardInteractor = keyguardInteractor, + sensitiveNotificationProtectionInteractor = sensitiveNotificationProtectionInteractor, + dumpManager = dumpManager, + ) + } + + @Before + fun setUp() { + kosmos.statusBarNotificationChipsInteractor.start() + } + + private fun Kosmos.buildPublicPrivatePromotedOngoing(): NotificationEntry = + buildPromotedOngoingEntry { + modifyNotification(applicationContext) + .setContentTitle("SENSITIVE") + .setPublicVersion( + Notification.Builder(applicationContext, "channel") + .setContentTitle("REDACTED") + .build() + ) + } + + @Test + fun content_sensitive_unlocked() = + kosmos.runTest { + // GIVEN a promoted entry + val ronEntry = buildPublicPrivatePromotedOngoing() + + setKeyguardLocked(false) + setScreenSharingProtectionActive(false) + + renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) + + // THEN aod content is sensitive + val content by collectLastValue(underTest.content) + assertThat(content?.title).isEqualTo("SENSITIVE") + } + + @Test + fun content_sensitive_locked() = + kosmos.runTest { + // GIVEN a promoted entry + val ronEntry = buildPublicPrivatePromotedOngoing() + + setKeyguardLocked(true) + setScreenSharingProtectionActive(false) + + renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) + + // THEN aod content is sensitive + val content by collectLastValue(underTest.content) + assertThat(content).isNotNull() + assertThat(content?.title).isNull() // SOON: .isEqualTo("REDACTED") + } + + @Test + fun content_sensitive_unlocked_screensharing() = + kosmos.runTest { + // GIVEN a promoted entry + val ronEntry = buildPublicPrivatePromotedOngoing() + + setKeyguardLocked(false) + setScreenSharingProtectionActive(true) + + renderNotificationListInteractor.setRenderedList(listOf(ronEntry)) + + // THEN aod content is sensitive + val content by collectLastValue(underTest.content) + assertThat(content).isNotNull() + assertThat(content?.title).isNull() // SOON: .isEqualTo("REDACTED") + } + + private fun Kosmos.setKeyguardLocked(locked: Boolean) { + fakeKeyguardRepository.setKeyguardDismissible(!locked) + } + + private fun Kosmos.setScreenSharingProtectionActive(active: Boolean) { + whenever(mockSensitiveNotificationProtectionController.isSensitiveStateActive) + .thenReturn(active) + whenever(mockSensitiveNotificationProtectionController.shouldProtectNotification(any())) + .thenReturn(active) + } +} 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 19b1046f1931..4aa21a68b2e0 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 @@ -399,35 +399,6 @@ public class NotificationContentInflaterTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled() - throws Exception { - final PromotedNotificationContentModels content = - new PromotedNotificationContentBuilder("key").build(); - mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); - - inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - - mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception { - final PromotedNotificationContentModels content = - new PromotedNotificationContentBuilder("key").build(); - mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); - - inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - - mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); - } - - @Test @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception { final PromotedNotificationContentModels content = 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 dcba3e447dda..21b0c9013b5f 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 @@ -465,32 +465,6 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { } @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() { - val content = PromotedNotificationContentBuilder("key").build() - promotedNotificationContentExtractor.resetForEntry(row.entry, content) - - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - - promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModels) - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() { - val content = PromotedNotificationContentBuilder("key").build() - promotedNotificationContentExtractor.resetForEntry(row.entry, content) - - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - - promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModels) - } - - @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_whenBothFlagsEnabled() { val content = PromotedNotificationContentBuilder("key").build() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 761ed6186afc..ca4dc0e5e546 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; + import static org.junit.Assert.assertNull; import android.app.Notification; @@ -64,6 +66,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { mContext, mDependency, TestableLooper.get(this)); + mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL); mGroup = mNotificationTestHelper.createGroup(); mChildrenContainer = mGroup.getChildrenContainer(); } @@ -172,9 +175,12 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() { + mChildrenContainer.setLowPriorityGroupHeader(null, null); mChildrenContainer.setIsMinimized(true); + + // THEN assertNull("We don't inflate header from the main thread with Async " - + "Inflation enabled", mChildrenContainer.getCurrentHeaderView()); + + "Inflation enabled", mChildrenContainer.getMinimizedNotificationHeader()); } @Test @@ -182,7 +188,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void setLowPriorityBeforeLowPriorityHeaderSet() { //Given: the children container does not have a low-priority header, and is not low-priority - assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); + mChildrenContainer.setLowPriorityGroupHeader(null, null); mGroup.setIsMinimized(false); //When: set the children container to be low-priority and set the low-priority header @@ -214,8 +220,8 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void changeLowPriorityAfterHeaderSet() { //Given: the children container does not have headers, and is not low-priority - assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); - assertNull(mChildrenContainer.getNotificationHeaderWrapper()); + mChildrenContainer.setLowPriorityGroupHeader(null, null); + mChildrenContainer.setGroupHeader(null, null); mGroup.setIsMinimized(false); //When: set the set the normal header diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt index e72d0c27e632..8aff622ee772 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt @@ -97,7 +97,7 @@ class MobileIconInteractorKairosAdapterTest : MobileIconInteractorTestBase() { } .asIncremental() .applyLatestSpecForKey(), - isStackable = interactor.isStackable.toState(), + isStackable = interactor.isStackable.toState(false), activeDataConnectionHasDataEnabled = interactor.activeDataConnectionHasDataEnabled.toState(), activeDataIconInteractor = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index 3d37914b1a7d..7dbcb270190c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -60,7 +60,6 @@ class MobileIconsViewModelTest : SysuiTestCase() { private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private lateinit var airplaneModeInteractor: AirplaneModeInteractor - @Mock private lateinit var constants: ConnectivityConstants @Mock private lateinit var logger: MobileViewLogger @Mock private lateinit var verboseLogger: VerboseMobileViewLogger @@ -84,7 +83,10 @@ class MobileIconsViewModelTest : SysuiTestCase() { verboseLogger, interactor, airplaneModeInteractor, - constants, + object : ConnectivityConstants { + override val hasDataCapabilities = true + override val shouldShowActivityConfig = false + }, testScope.backgroundScope, ) @@ -349,7 +351,42 @@ class MobileIconsViewModelTest : SysuiTestCase() { // WHEN sub2 becomes last and sub2 has a network type icon interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - // THEN the flow updates + assertThat(latest).isTrue() + job.cancel() + } + + @Test + fun isStackable_apmEnabled_false() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isStackable.onEach { latest = it }.launchIn(this) + + // Set the interactor to true to test APM + interactor.isStackable.value = true + + // Enable APM + airplaneModeInteractor.setIsAirplaneMode(true) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + + assertThat(latest).isFalse() + job.cancel() + } + + @Test + fun isStackable_apmDisabled_true() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isStackable.onEach { latest = it }.launchIn(this) + + // Set the interactor to true to test APM + interactor.isStackable.value = true + + // Disable APM + airplaneModeInteractor.setIsAirplaneMode(false) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + assertThat(latest).isTrue() job.cancel() diff --git a/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml b/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml new file mode 100644 index 000000000000..bc62d38f1932 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_accessibility.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,240Q447,240 423.5,216.5Q400,193 400,160Q400,127 423.5,103.5Q447,80 480,80Q513,80 536.5,103.5Q560,127 560,160Q560,193 536.5,216.5Q513,240 480,240ZM360,840L360,360Q311,356 261,349Q211,342 163,331Q146,327 135.5,312Q125,297 130,280Q135,263 151,255Q167,247 185,251Q255,266 330.5,273Q406,280 480,280Q554,280 629.5,273Q705,266 775,251Q793,247 809,255Q825,263 830,280Q835,297 824.5,312Q814,327 797,331Q749,342 699,349Q649,356 600,360L600,840Q600,857 588.5,868.5Q577,880 560,880Q543,880 531.5,868.5Q520,857 520,840L520,640L440,640L440,840Q440,857 428.5,868.5Q417,880 400,880Q383,880 371.5,868.5Q360,857 360,840Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml b/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml new file mode 100644 index 000000000000..91644873c064 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_connectivty.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,840Q438,840 409,811Q380,782 380,740Q380,698 409,669Q438,640 480,640Q522,640 551,669Q580,698 580,740Q580,782 551,811Q522,840 480,840ZM480,400Q555,400 622.5,424Q690,448 745,490Q765,505 765.5,529.5Q766,554 748,572Q731,589 706,589.5Q681,590 661,576Q623,550 577,535Q531,520 480,520Q429,520 383,535Q337,550 299,576Q279,590 254,589Q229,588 212,571Q195,553 195,528.5Q195,504 215,489Q270,447 337.5,423.5Q405,400 480,400ZM480,160Q605,160 715.5,201Q826,242 914,317Q934,334 935,359Q936,384 918,402Q901,419 876,419.5Q851,420 831,404Q759,345 669.5,312.5Q580,280 480,280Q380,280 290.5,312.5Q201,345 129,404Q109,420 84,419.5Q59,419 42,402Q24,384 25,359Q26,334 46,317Q134,242 244.5,201Q355,160 480,160Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_display.xml b/packages/SystemUI/res/drawable/ic_qs_category_display.xml new file mode 100644 index 000000000000..c238e940eb01 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_display.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M346,800L240,800Q207,800 183.5,776.5Q160,753 160,720L160,614L83,536Q72,524 66,509.5Q60,495 60,480Q60,465 66,450.5Q72,436 83,424L160,346L160,240Q160,207 183.5,183.5Q207,160 240,160L346,160L424,83Q436,72 450.5,66Q465,60 480,60Q495,60 509.5,66Q524,72 536,83L614,160L720,160Q753,160 776.5,183.5Q800,207 800,240L800,346L877,424Q888,436 894,450.5Q900,465 900,480Q900,495 894,509.5Q888,524 877,536L800,614L800,720Q800,753 776.5,776.5Q753,800 720,800L614,800L536,877Q524,888 509.5,894Q495,900 480,900Q465,900 450.5,894Q436,888 424,877L346,800ZM380,720L480,820Q480,820 480,820Q480,820 480,820L580,720L720,720Q720,720 720,720Q720,720 720,720L720,580L820,480Q820,480 820,480Q820,480 820,480L720,380L720,240Q720,240 720,240Q720,240 720,240L580,240L480,140Q480,140 480,140Q480,140 480,140L380,240L240,240Q240,240 240,240Q240,240 240,240L240,380L140,480Q140,480 140,480Q140,480 140,480L240,580L240,720Q240,720 240,720Q240,720 240,720L380,720ZM480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280L480,680Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml b/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml new file mode 100644 index 000000000000..915cf41ba1f6 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_privacy.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M444,600L516,600Q525,600 531.5,592.5Q538,585 536,576L517,471Q537,461 548.5,442Q560,423 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,423 411.5,442Q423,461 443,471L424,576Q422,585 428.5,592.5Q435,600 444,600ZM480,876Q473,876 467,875Q461,874 455,872Q320,827 240,705.5Q160,584 160,444L160,255Q160,230 174.5,210Q189,190 212,181L452,91Q466,86 480,86Q494,86 508,91L748,181Q771,190 785.5,210Q800,230 800,255L800,444Q800,584 720,705.5Q640,827 505,872Q499,874 493,875Q487,876 480,876Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml b/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml new file mode 100644 index 000000000000..cea43ae1bc2f --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_provided_by_apps.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M240,800Q207,800 183.5,776.5Q160,753 160,720Q160,687 183.5,663.5Q207,640 240,640Q273,640 296.5,663.5Q320,687 320,720Q320,753 296.5,776.5Q273,800 240,800ZM480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM720,800Q687,800 663.5,776.5Q640,753 640,720Q640,687 663.5,663.5Q687,640 720,640Q753,640 776.5,663.5Q800,687 800,720Q800,753 776.5,776.5Q753,800 720,800ZM240,560Q207,560 183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM720,560Q687,560 663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560ZM240,320Q207,320 183.5,296.5Q160,273 160,240Q160,207 183.5,183.5Q207,160 240,160Q273,160 296.5,183.5Q320,207 320,240Q320,273 296.5,296.5Q273,320 240,320ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320ZM720,320Q687,320 663.5,296.5Q640,273 640,240Q640,207 663.5,183.5Q687,160 720,160Q753,160 776.5,183.5Q800,207 800,240Q800,273 776.5,296.5Q753,320 720,320Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml b/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml new file mode 100644 index 000000000000..ec2ce15e2d01 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_unknown.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640L200,760Q200,760 200,760Q200,760 200,760L320,760Q337,760 348.5,771.5Q360,783 360,800Q360,817 348.5,828.5Q337,840 320,840L200,840ZM760,840L640,840Q623,840 611.5,828.5Q600,817 600,800Q600,783 611.5,771.5Q623,760 640,760L760,760Q760,760 760,760Q760,760 760,760L760,640Q760,623 771.5,611.5Q783,600 800,600Q817,600 828.5,611.5Q840,623 840,640L840,760Q840,793 816.5,816.5Q793,840 760,840ZM120,200Q120,167 143.5,143.5Q167,120 200,120L320,120Q337,120 348.5,131.5Q360,143 360,160Q360,177 348.5,188.5Q337,200 320,200L200,200Q200,200 200,200Q200,200 200,200L200,320Q200,337 188.5,348.5Q177,360 160,360Q143,360 131.5,348.5Q120,337 120,320L120,200ZM840,200L840,320Q840,337 828.5,348.5Q817,360 800,360Q783,360 771.5,348.5Q760,337 760,320L760,200Q760,200 760,200Q760,200 760,200L640,200Q623,200 611.5,188.5Q600,177 600,160Q600,143 611.5,131.5Q623,120 640,120L760,120Q793,120 816.5,143.5Q840,167 840,200ZM480,720Q501,720 515.5,705.5Q530,691 530,670Q530,649 515.5,634.5Q501,620 480,620Q459,620 444.5,634.5Q430,649 430,670Q430,691 444.5,705.5Q459,720 480,720ZM480,308Q506,308 525.5,324Q545,340 545,365Q545,388 530.5,406Q516,424 499,439Q473,462 459.5,482.5Q446,503 444,532Q443,546 454,556.5Q465,567 480,567Q494,567 505.5,557Q517,547 519,532Q521,515 531,502Q541,489 560,470Q595,435 606.5,413.5Q618,392 618,362Q618,308 579,274Q540,240 480,240Q439,240 406.5,258.5Q374,277 357,311Q351,323 356.5,335.5Q362,348 375,353Q388,358 401.5,353Q415,348 423,337Q434,323 448.5,315.5Q463,308 480,308Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml b/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml new file mode 100644 index 000000000000..4dfac8393b8e --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_category_utilities.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M433,880Q406,880 386.5,862Q367,844 363,818L354,752Q341,747 329.5,740Q318,733 307,725L245,751Q220,762 195,753Q170,744 156,721L109,639Q95,616 101,590Q107,564 128,547L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L128,413Q107,396 101,370Q95,344 109,321L156,239Q170,216 195,207Q220,198 245,209L307,235Q318,227 330,220Q342,213 354,208L363,142Q367,116 386.5,98Q406,80 433,80L527,80Q554,80 573.5,98Q593,116 597,142L606,208Q619,213 630.5,220Q642,227 653,235L715,209Q740,198 765,207Q790,216 804,239L851,321Q865,344 859,370Q853,396 832,413L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L831,547Q852,564 858,590Q864,616 850,639L802,721Q788,744 763,753Q738,762 713,751L653,725Q642,733 630,740Q618,747 606,752L597,818Q593,844 573.5,862Q554,880 527,880L433,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml index 4002f7808637..1e4a07f5fc30 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml @@ -22,7 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" - android:importantForAccessibility="yes" + android:importantForAccessibility="auto" android:clickable="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/rightGuideline" diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml index 3c8cb6860a41..8234c24a7e17 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml @@ -23,7 +23,7 @@ android:layout_height="match_parent"> android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" - android:importantForAccessibility="yes" + android:importantForAccessibility="auto" android:clickable="false" android:paddingHorizontal="16dp" android:paddingVertical="16dp" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 5c06585de99c..65d17042412b 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -85,7 +85,7 @@ frame when animating QS <-> QQS transition android:paddingEnd="@dimen/status_bar_left_clock_end_padding" android:singleLine="true" android:textDirection="locale" - android:textAppearance="@style/TextAppearance.QS.Status" + android:textAppearance="@style/TextAppearance.QS.Status.Clock" android:fontFeatureSettings="tnum" android:transformPivotX="0dp" android:transformPivotY="24dp" diff --git a/packages/SystemUI/res/layout/notification_conversation_info.xml b/packages/SystemUI/res/layout/notification_conversation_info.xml index 9560be0d6969..56660139f823 100644 --- a/packages/SystemUI/res/layout/notification_conversation_info.xml +++ b/packages/SystemUI/res/layout/notification_conversation_info.xml @@ -391,6 +391,16 @@ android:paddingEnd="4dp" > <TextView + android:id="@+id/inline_dismiss" + android:text="@string/notification_inline_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView android:id="@+id/done" android:text="@string/inline_ok_button" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index ff16e063f5b1..fe65f32c6eb0 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -148,6 +148,7 @@ <!-- Animated Action colors --> <color name="animated_action_button_text_color">@androidprv:color/materialColorOnSurface</color> <color name="animated_action_button_stroke_color">@androidprv:color/materialColorOnSurface</color> + <color name="animated_action_button_attribution_color">@androidprv:color/materialColorOnSurfaceVariant</color> <!-- Biometric dialog colors --> <color name="biometric_dialog_gray">#ff757575</color> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2d40c32e29e9..50242f69a755 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2354,8 +2354,8 @@ <string name="group_system_access_all_apps_search">Open apps list</string> <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] --> <string name="group_system_access_system_settings">Open settings</string> - <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] --> - <string name="group_system_access_google_assistant">Open assistant</string> + <!-- User visible title for the keyboard shortcut that accesses the default digital assistant app. [CHAR LIMIT=70] --> + <string name="group_system_access_google_assistant">Open digital assistant</string> <!-- User visible title for the keyboard shortcut that locks screen. [CHAR LIMIT=70] --> <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 0e1f99f28850..bde750145ff7 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -181,15 +181,19 @@ <item name="android:textColor">@androidprv:color/materialColorOnSurfaceVariant</item> </style> - <!-- This is hard coded to be sans-serif-condensed to match the icons --> - <style name="TextAppearance.QS.Status"> - <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="!com.android.systemui.shade_header_font_update">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="com.android.systemui.shade_header_font_update">variable-body-medium-emphasized</item> <item name="android:textColor">@color/shade_header_text_color</item> <item name="android:textSize">14sp</item> <item name="android:letterSpacing">0.01</item> </style> + <style name="TextAppearance.QS.Status.Clock"> + <item name="android:fontFamily" android:featureFlag="!com.android.systemui.shade_header_font_update">@*android:string/config_headlineFontFamily</item> + <item name="android:fontFamily" android:featureFlag="com.android.systemui.shade_header_font_update">variable-display-small-emphasized</item> + </style> + <style name="TextAppearance.QS.Status.Build"> <item name="android:textColor">?attr/onShadeInactiveVariant</item> </style> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt index 8a5e011cd3ce..2bb9809af30e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.domain.interactor +import android.annotation.SuppressLint import android.content.Context import android.hardware.fingerprint.FingerprintManager import android.util.Log @@ -32,10 +33,14 @@ import javax.inject.Inject import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -131,6 +136,25 @@ constructor( } .distinctUntilChanged() + /** + * Event flow that emits every time the user taps the screen and a UDFPS guidance message is + * surfaced and then cleared. Modeled as a SharedFlow because a StateFlow fails to emit every + * event to the subscriber, causing missed Talkback feedback and incorrect focusability state of + * the UDFPS accessibility overlay. + */ + @SuppressLint("SharedFlowCreation") + private val _clearAccessibilityOverlayMessageReason = MutableSharedFlow<String?>() + + /** Indicates the reason for clearing the UDFPS accessibility overlay content description */ + val clearAccessibilityOverlayMessageReason: SharedFlow<String?> = + _clearAccessibilityOverlayMessageReason.asSharedFlow() + + suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) { + // Add delay to make sure we read the guidance message before clearing it + delay(1000) + _clearAccessibilityOverlayMessageReason.emit(reason) + } + companion object { private const val TAG = "UdfpsOverlayInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 3b22e13f29a2..80d06f4a2d37 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -27,6 +27,7 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES import android.view.accessibility.AccessibilityManager import android.widget.Button import android.widget.ImageView @@ -43,7 +44,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.biometrics.Utils.ellipsize import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality @@ -63,6 +63,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch private const val TAG = "BiometricViewBinder" @@ -123,25 +124,6 @@ object BiometricViewBinder { val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) val retryButton = view.requireViewById<Button>(R.id.button_try_again) - // Handles custom "Cancel Authentication" talkback action - val cancelDelegate: AccessibilityDelegateCompat = - object : AccessibilityDelegateCompat() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfoCompat, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction( - AccessibilityActionCompat( - AccessibilityNodeInfoCompat.ACTION_CLICK, - view.context.getString(R.string.biometric_dialog_cancel_authentication), - ) - ) - } - } - ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate) - ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate) - // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( @@ -155,6 +137,33 @@ object BiometricViewBinder { var boundSize = false view.repeatWhenAttached { + // Handles custom "Cancel Authentication" talkback action + val cancelDelegate: AccessibilityDelegateCompat = + object : AccessibilityDelegateCompat() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + lifecycleScope.launch { + // Clears UDFPS guidance hint after focus moves to cancel view + viewModel.onClearUdfpsGuidanceHint( + accessibilityManager.isTouchExplorationEnabled + ) + } + info.addAction( + AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, + view.context.getString( + R.string.biometric_dialog_cancel_authentication + ), + ) + ) + } + } + ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate) + ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate) + // these do not change and need to be set before any size transitions val modalities = viewModel.modalities.first() @@ -404,11 +413,16 @@ object BiometricViewBinder { } false } + launch { viewModel.accessibilityHint.collect { message -> - if (message.isNotBlank()) { - udfpsGuidanceView.contentDescription = message - } + udfpsGuidanceView.importantForAccessibility = + if (message == null) { + IMPORTANT_FOR_ACCESSIBILITY_NO + } else { + IMPORTANT_FOR_ACCESSIBILITY_YES + } + udfpsGuidanceView.contentDescription = message } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 4e17a2658ee7..27fc1878cc99 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -187,10 +187,10 @@ constructor( } } - private val _accessibilityHint = MutableSharedFlow<String>() + private val _accessibilityHint = MutableSharedFlow<String?>() /** Hint for talkback directional guidance */ - val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() + val accessibilityHint: Flow<String?> = _accessibilityHint.asSharedFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -923,6 +923,19 @@ constructor( return false } + /** Clears the message used for UDFPS directional guidance */ + suspend fun onClearUdfpsGuidanceHint(touchExplorationEnabled: Boolean) { + if ( + modalities.first().hasUdfps && + touchExplorationEnabled && + !isAuthenticated.first().isAuthenticated + ) { + // Add delay to make sure we read the guidance message before clearing it + delay(1000) + _accessibilityHint.emit(null) + } + } + /** * Switch to the credential view. * diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 6aeb35b3b158..99f299918969 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -73,6 +74,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.modifiers.padding +import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.drawInOverlay import com.android.systemui.Flags import com.android.systemui.biometrics.Utils.toBitmap @@ -139,7 +141,7 @@ fun BrightnessSlider( } else { null } - val colors = SliderDefaults.colors() + val colors = colors() // The value state is recreated every time gammaValue changes, so we recreate this derivedState // We have to use value as that's the value that changes when the user is dragging (gammaValue @@ -211,6 +213,7 @@ fun BrightnessSlider( interactionSource = interactionSource, enabled = enabled, thumbSize = DpSize(4.dp, 52.dp), + colors = colors, ) }, track = { sliderState -> @@ -293,6 +296,7 @@ fun BrightnessSlider( trackInsideCornerSize = 2.dp, drawStopIndicator = null, thumbTrackGapSize = ThumbTrackGapSize, + colors = colors, ) }, ) @@ -441,3 +445,13 @@ object BrightnessSliderMotionTestKeys { val ActiveIconAlpha = MotionTestValueKey<Float>("activeIconAlpha") val InactiveIconAlpha = MotionTestValueKey<Float>("inactiveIconAlpha") } + +@Composable +private fun colors(): SliderColors { + return SliderDefaults.colors() + .copy( + inactiveTrackColor = LocalAndroidColorScheme.current.surfaceEffect2, + activeTickColor = MaterialTheme.colorScheme.onPrimary, + inactiveTickColor = MaterialTheme.colorScheme.onSurface, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt index 8cebe04d4e01..96dbcc5867c1 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt @@ -21,20 +21,30 @@ import android.content.ClipDescription import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException import android.net.Uri import android.text.TextUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @SysUISingleton class ActionIntentCreator @Inject -constructor(@Application private val applicationScope: CoroutineScope) : IntentCreator { +constructor( + private val context: Context, + private val packageManager: PackageManager, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : IntentCreator { override fun getTextEditorIntent(context: Context?) = Intent(context, EditTextActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -72,11 +82,9 @@ constructor(@Application private val applicationScope: CoroutineScope) : IntentC } suspend fun getImageEditIntent(uri: Uri?, context: Context): Intent { - val editorPackage = context.getString(R.string.config_screenshotEditor) return Intent(Intent.ACTION_EDIT).apply { - if (!TextUtils.isEmpty(editorPackage)) { - setComponent(ComponentName.unflattenFromString(editorPackage)) - } + // Use the preferred editor if it's available, otherwise fall back to the default editor + component = preferredEditor() ?: defaultEditor() setDataAndType(uri, "image/*") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -105,6 +113,39 @@ constructor(@Application private val applicationScope: CoroutineScope) : IntentC } } + private suspend fun preferredEditor(): ComponentName? = + runCatching { + val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor) + val component = ComponentName.unflattenFromString(preferredEditor) ?: return null + + return if (isComponentAvailable(component)) component else null + } + .getOrNull() + + private suspend fun isComponentAvailable(component: ComponentName): Boolean = + withContext(backgroundDispatcher) { + try { + val info = + packageManager.getPackageInfo( + component.packageName, + PackageManager.GET_ACTIVITIES, + ) + info.activities?.firstOrNull { + it.componentName.className == component.className + } != null + } catch (e: NameNotFoundException) { + false + } + } + + private fun defaultEditor(): ComponentName? = + runCatching { + context.getString(R.string.config_screenshotEditor).let { + ComponentName.unflattenFromString(it) + } + } + .getOrNull() + companion object { private const val EXTRA_EDIT_SOURCE: String = "edit_source" private const val EDIT_SOURCE_CLIPBOARD: String = "clipboard" diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index a31c0bd35453..2875b7e2ae92 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -108,6 +108,9 @@ interface CommunalModule { const val LAUNCHER_PACKAGE = "launcher_package" const val SWIPE_TO_HUB = "swipe_to_hub" const val SHOW_UMO = "show_umo" + const val TOUCH_NOTIFICATION_RATE_LIMIT = "TOUCH_NOTIFICATION_RATE_LIMIT" + + const val TOUCH_NOTIFIFCATION_RATE_LIMIT_MS = 100 @Provides @Communal @@ -159,5 +162,11 @@ interface CommunalModule { fun provideShowUmo(@Main resources: Resources): Boolean { return resources.getBoolean(R.bool.config_showUmoOnHub) } + + @Provides + @Named(TOUCH_NOTIFICATION_RATE_LIMIT) + fun providesRateLimit(): Int { + return TOUCH_NOTIFIFCATION_RATE_LIMIT_MS + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index bf4445ba18db..2b8cf008c0c7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.data.repository +import android.content.res.Configuration import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey @@ -49,6 +50,9 @@ interface CommunalSceneRepository { /** Exposes the transition state of the communal [SceneTransitionLayout]. */ val transitionState: StateFlow<ObservableTransitionState> + /** Current orientation of the communal container. */ + val communalContainerOrientation: StateFlow<Int> + /** Updates the requested scene. */ fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) @@ -64,6 +68,9 @@ interface CommunalSceneRepository { * Note that you must call is with `null` when the UI is done or risk a memory leak. */ fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) + + /** Set the current orientation of the communal container. */ + fun setCommunalContainerOrientation(orientation: Int) } @SysUISingleton @@ -89,6 +96,11 @@ constructor( initialValue = defaultTransitionState, ) + private val _communalContainerOrientation = + MutableStateFlow(Configuration.ORIENTATION_UNDEFINED) + override val communalContainerOrientation: StateFlow<Int> = + _communalContainerOrientation.asStateFlow() + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { applicationScope.launch { // SceneTransitionLayout state updates must be triggered on the thread the STL was @@ -105,6 +117,10 @@ constructor( } } + override fun setCommunalContainerOrientation(orientation: Int) { + _communalContainerOrientation.value = orientation + } + override suspend fun showHubFromPowerButton() { // If keyguard is not showing yet, the hub view is not ready and the // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource] diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index 42a345b7deb4..8d599541b184 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -206,8 +206,11 @@ constructor( } .flowOn(bgDispatcher) - override fun getWhenToStartHubState(user: UserInfo): Flow<WhenToStartHub> = - secureSettings + override fun getWhenToStartHubState(user: UserInfo): Flow<WhenToStartHub> { + if (!getV2FlagEnabled()) { + return MutableStateFlow(WhenToStartHub.NEVER) + } + return secureSettings .observerFlow( userId = user.id, names = arrayOf(Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB), @@ -225,11 +228,13 @@ constructor( Settings.Secure.GLANCEABLE_HUB_START_CHARGING -> WhenToStartHub.WHILE_CHARGING Settings.Secure.GLANCEABLE_HUB_START_CHARGING_UPRIGHT -> WhenToStartHub.WHILE_CHARGING_AND_POSTURED + Settings.Secure.GLANCEABLE_HUB_START_DOCKED -> WhenToStartHub.WHILE_DOCKED else -> WhenToStartHub.NEVER } } .flowOn(bgDispatcher) + } override fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> = broadcastDispatcher diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index fed99d71fa3b..a112dd25e006 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.res.Configuration import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey @@ -32,6 +33,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.pairwiseBy import javax.inject.Inject @@ -58,6 +60,7 @@ constructor( private val repository: CommunalSceneRepository, private val logger: CommunalSceneLogger, private val sceneInteractor: SceneInteractor, + private val keyguardStateController: KeyguardStateController, ) { private val _isLaunchingWidget = MutableStateFlow(false) @@ -68,6 +71,30 @@ constructor( _isLaunchingWidget.value = launching } + /** + * Whether screen will be rotated to portrait if transitioned out of hub to keyguard screens. + */ + var willRotateToPortrait: Flow<Boolean> = + repository.communalContainerOrientation + .map { + it == Configuration.ORIENTATION_LANDSCAPE && + !keyguardStateController.isKeyguardScreenRotationAllowed() + } + .distinctUntilChanged() + + /** Whether communal container is rotated to portrait. Emits an initial value of false. */ + val rotatedToPortrait: StateFlow<Boolean> = + repository.communalContainerOrientation + .pairwiseBy(initialValue = false) { old, new -> + old == Configuration.ORIENTATION_LANDSCAPE && + new == Configuration.ORIENTATION_PORTRAIT + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + fun setCommunalContainerOrientation(orientation: Int) { + repository.setCommunalContainerOrientation(orientation) + } + fun interface OnSceneAboutToChangeListener { /** Notifies that the scene is about to change to [toScene]. */ fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt index e487590d87d7..25c7f477c815 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt @@ -20,7 +20,6 @@ import android.annotation.SuppressLint import android.hardware.Sensor import android.hardware.TriggerEvent import android.hardware.TriggerEventListener -import android.service.dreams.Flags.allowDreamWhenPostured import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.PosturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState @@ -170,15 +169,10 @@ constructor( * NOTE: Due to smoothing, this signal may be delayed to ensure we have a stable reading before * being considered postured. */ - val postured: Flow<Boolean> by lazy { - if (allowDreamWhenPostured()) { - combine(posturedSmoothed, debugPostured) { postured, debugValue -> - debugValue.asBoolean() ?: postured.asBoolean() ?: false - } - } else { - MutableStateFlow(false) + val postured: Flow<Boolean> = + combine(posturedSmoothed, debugPostured) { postured, debugValue -> + debugValue.asBoolean() ?: postured.asBoolean() ?: false } - } /** * Helper for observing a trigger sensor, which automatically unregisters itself after it diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index a84c45732169..49dc59ac0004 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -33,4 +33,6 @@ object CommunalTransitionKeys { val FromEditMode = TransitionKey("FromEditMode") /** Swipes the glanceable hub in/out of view */ val Swipe = TransitionKey("Swipe") + /** Swipes out of glanceable hub in landscape orientation */ + val SwipeInLandscape = TransitionKey("SwipeInLandscape") } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 5a4b0b0e2d24..a6309d1be03d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -386,6 +386,11 @@ constructor( } } + val swipeFromHubInLandscape: Flow<Boolean> = communalSceneInteractor.willRotateToPortrait + + fun onOrientationChange(orientation: Int) = + communalSceneInteractor.setCommunalContainerOrientation(orientation) + companion object { const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L } diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt b/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt new file mode 100644 index 000000000000..fec98a311fbd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt @@ -0,0 +1,58 @@ +/* + * 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.communal.util + +import android.view.MotionEvent +import com.android.systemui.communal.dagger.CommunalModule.Companion.TOUCH_NOTIFICATION_RATE_LIMIT +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.power.domain.interactor.PowerInteractor +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * {@link UserTouchActivityNotifier} helps rate limit the user activity notifications sent to {@link + * PowerManager} from a single touch source. + */ +class UserTouchActivityNotifier +@Inject +constructor( + @Background private val scope: CoroutineScope, + private val powerInteractor: PowerInteractor, + @Named(TOUCH_NOTIFICATION_RATE_LIMIT) private val rateLimitMs: Int, +) { + private var lastNotification: Long? = null + + fun notifyActivity(event: MotionEvent) { + val metered = + when (event.action) { + MotionEvent.ACTION_CANCEL -> false + MotionEvent.ACTION_UP -> false + MotionEvent.ACTION_DOWN -> false + else -> true + } + + if (metered && lastNotification?.let { event.eventTime - it < rateLimitMs } == true) { + return + } + + lastNotification = event.eventTime + + scope.launch { powerInteractor.onUserTouch() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 0d0105404726..1e50205500f9 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -20,6 +20,7 @@ import android.app.trust.TrustManager import android.content.Context import android.hardware.biometrics.BiometricFaceConstants import android.hardware.biometrics.BiometricSourceType +import android.service.dreams.Flags.dreamsV2 import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.biometrics.shared.model.LockoutMode @@ -40,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.TransitionState @@ -136,11 +138,18 @@ constructor( } .launchIn(applicationScope) - merge( - keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN)), - keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN)), - keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)), - ) + val transitionFlows = buildList { + add(keyguardTransitionInteractor.transition(Edge.create(AOD, LOCKSCREEN))) + add(keyguardTransitionInteractor.transition(Edge.create(OFF, LOCKSCREEN))) + add(keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN))) + + if (dreamsV2()) { + add(keyguardTransitionInteractor.transition(Edge.create(DREAMING, LOCKSCREEN))) + } + } + + transitionFlows + .merge() .filter { it.transitionState == TransitionState.STARTED } .sample(powerInteractor.detailedWakefulness) .filter { wakefulnessModel -> diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt index e2172d0773d3..3abc260fdcbd 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/UdfpsAccessibilityOverlayBinder.kt @@ -18,27 +18,58 @@ package com.android.systemui.deviceentry.ui.binder import android.annotation.SuppressLint +import android.util.Log +import android.view.MotionEvent +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES import androidx.core.view.isInvisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel import com.android.systemui.lifecycle.repeatWhenAttached object UdfpsAccessibilityOverlayBinder { + private const val TAG = "UdfpsAccessibilityOverlayBinder" /** Forwards hover events to the view model to make guided announcements for accessibility. */ @SuppressLint("ClickableViewAccessibility") @JvmStatic - fun bind( - view: UdfpsAccessibilityOverlay, - viewModel: UdfpsAccessibilityOverlayViewModel, - ) { - view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) } + fun bind(view: UdfpsAccessibilityOverlay, viewModel: UdfpsAccessibilityOverlayViewModel) { view.repeatWhenAttached { // Repeat on CREATED because we update the visibility of the view repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.visible.collect { visible -> view.isInvisible = !visible } + view.setOnHoverListener { v, event -> + if (event.action == MotionEvent.ACTION_HOVER_ENTER) { + launch { viewModel.onHoverEvent(v, event) } + } + false + } + + launch { viewModel.visible.collect { visible -> view.isInvisible = !visible } } + + launch { + viewModel.contentDescription.collect { contentDescription -> + if (contentDescription != null) { + view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES + view.contentDescription = contentDescription + } + } + } + + launch { + viewModel.clearAccessibilityOverlayMessageReason.collect { reason -> + Log.d( + TAG, + "clearing content description of UDFPS accessibility overlay " + + "for reason: $reason", + ) + view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + view.contentDescription = null + viewModel.setContentDescription(null) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt index 9c3b9b273ab5..0a2d10d10a40 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt @@ -23,5 +23,7 @@ import android.view.View class UdfpsAccessibilityOverlay(context: Context?) : View(context) { init { accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO + isClickable = false } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt index 5c7cd5f55942..22ed6da2e5bf 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/AlternateBouncerUdfpsAccessibilityOverlayViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.deviceentry.ui.viewmodel import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -26,13 +27,23 @@ import kotlinx.coroutines.flow.flowOf class AlternateBouncerUdfpsAccessibilityOverlayViewModel @Inject constructor( - udfpsOverlayInteractor: UdfpsOverlayInteractor, + private val udfpsOverlayInteractor: UdfpsOverlayInteractor, accessibilityInteractor: AccessibilityInteractor, + udfpsUtils: UdfpsUtils, ) : UdfpsAccessibilityOverlayViewModel( udfpsOverlayInteractor, accessibilityInteractor, + udfpsUtils, ) { /** Overlay is always visible if touch exploration is enabled on the alternate bouncer. */ override fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> = flowOf(true) + + /** + * Clears the content description to prevent the view from storing stale UDFPS directional + * guidance messages for accessibility. + */ + suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) { + udfpsOverlayInteractor.clearUdfpsAccessibilityOverlayMessage(reason) + } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt index b84d65a2b430..5c86514775de 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/DeviceEntryUdfpsAccessibilityOverlayViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.deviceentry.ui.viewmodel import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel @@ -33,10 +34,12 @@ constructor( accessibilityInteractor: AccessibilityInteractor, private val deviceEntryIconViewModel: DeviceEntryIconViewModel, private val deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel, + udfpsUtils: UdfpsUtils, ) : UdfpsAccessibilityOverlayViewModel( udfpsOverlayInteractor, accessibilityInteractor, + udfpsUtils, ) { /** Overlay is only visible if the UDFPS icon is visible on the keyguard. */ override fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt index 1849bf20abdb..a58f3681555c 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt @@ -24,7 +24,10 @@ import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -32,8 +35,17 @@ import kotlinx.coroutines.flow.flowOf abstract class UdfpsAccessibilityOverlayViewModel( udfpsOverlayInteractor: UdfpsOverlayInteractor, accessibilityInteractor: AccessibilityInteractor, + private val udfpsUtils: UdfpsUtils, ) { - private val udfpsUtils = UdfpsUtils() + /** Indicates the reason for clearing the UDFPS accessibility overlay content description */ + val clearAccessibilityOverlayMessageReason: SharedFlow<String?> = + udfpsOverlayInteractor.clearAccessibilityOverlayMessageReason + + private val _contentDescription: MutableStateFlow<CharSequence?> = MutableStateFlow(null) + + /** Content description of the UDFPS accessibility overlay */ + val contentDescription: Flow<CharSequence?> = _contentDescription.asStateFlow() + private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> = udfpsOverlayInteractor.udfpsOverlayParams @@ -46,6 +58,10 @@ abstract class UdfpsAccessibilityOverlayViewModel( } } + fun setContentDescription(contentDescription: CharSequence?) { + _contentDescription.value = contentDescription + } + abstract fun isVisibleWhenTouchExplorationEnabled(): Flow<Boolean> /** Give directional feedback to help the user authenticate with UDFPS. */ @@ -77,8 +93,9 @@ abstract class UdfpsAccessibilityOverlayViewModel( overlayParams, /* touchRotatedToPortrait */ false, ) + if (announceStr != null) { - v.contentDescription = announceStr + _contentDescription.value = announceStr } } // always let the motion events go through to underlying views diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index f2a10cc43fd9..8e857b3313a7 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -58,7 +58,9 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.PowerManager; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -194,6 +196,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene static final String GLOBAL_ACTION_KEY_EMERGENCY = "emergency"; static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot"; static final String GLOBAL_ACTION_KEY_SYSTEM_UPDATE = "system_update"; + static final String GLOBAL_ACTION_KEY_STANDBY = "standby"; // See NotificationManagerService#scheduleDurationReachedLocked private static final long TOAST_FADE_TIME = 333; @@ -270,6 +273,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final UserLogoutInteractor mLogoutInteractor; private final GlobalActionsInteractor mInteractor; private final Lazy<DisplayWindowPropertiesRepository> mDisplayWindowPropertiesRepositoryLazy; + private final PowerManager mPowerManager; private final Handler mHandler; private final UserTracker.Callback mOnUserSwitched = new UserTracker.Callback() { @@ -341,7 +345,10 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene GA_CLOSE_POWER_VOLUP(811), @UiEvent(doc = "System Update button was pressed.") - GA_SYSTEM_UPDATE_PRESS(1716); + GA_SYSTEM_UPDATE_PRESS(1716), + + @UiEvent(doc = "The global actions standby button was pressed.") + GA_STANDBY_PRESS(2210); private final int mId; @@ -396,7 +403,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene SelectedUserInteractor selectedUserInteractor, UserLogoutInteractor logoutInteractor, GlobalActionsInteractor interactor, - Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository) { + Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository, + PowerManager powerManager) { mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; @@ -434,6 +442,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mLogoutInteractor = logoutInteractor; mInteractor = interactor; mDisplayWindowPropertiesRepositoryLazy = displayWindowPropertiesRepository; + mPowerManager = powerManager; mHandler = new Handler(mMainHandler.getLooper()) { public void handleMessage(Message msg) { @@ -697,6 +706,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } else if (GLOBAL_ACTION_KEY_SYSTEM_UPDATE.equals(actionKey)) { addIfShouldShowAction(tempActions, new SystemUpdateAction()); + } else if (GLOBAL_ACTION_KEY_STANDBY.equals(actionKey)) { + addIfShouldShowAction(tempActions, new StandbyAction()); } else { Log.e(TAG, "Invalid global action key " + actionKey); } @@ -1245,6 +1256,36 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } + @VisibleForTesting + class StandbyAction extends SinglePressAction { + StandbyAction() { + super(R.drawable.ic_standby, R.string.global_action_standby); + } + + @Override + public void onPress() { + // Add a little delay before executing, to give the dialog a chance to go away before + // going to sleep. Otherwise, we see screen flicker randomly. + mHandler.postDelayed(() -> { + mUiEventLogger.log(GlobalActionsEvent.GA_STANDBY_PRESS); + mBackgroundExecutor.execute(() -> { + mPowerManager.goToSleep(SystemClock.uptimeMillis(), + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0); + }); + }, mDialogPressDelay); + } + + @Override + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return true; + } + } + private Action getSettingsAction() { return new SinglePressAction(R.drawable.ic_settings, R.string.global_action_settings) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt index 7c4dbfeba50f..7110c37e88e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt @@ -20,11 +20,13 @@ import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DreamingToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel @@ -81,6 +83,10 @@ interface PrimaryBouncerTransitionImplModule { @Binds @IntoSet + fun fromDreaming(impl: DreamingToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet fun toAod(impl: PrimaryBouncerToAodTransitionViewModel): PrimaryBouncerTransition @Binds @@ -103,5 +109,9 @@ interface PrimaryBouncerTransitionImplModule { @Binds @IntoSet + fun toDreaming(impl: PrimaryBouncerToDreamingTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet fun toOccluded(impl: PrimaryBouncerToOccludedTransitionViewModel): PrimaryBouncerTransition } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 3b1b6fcc45f2..09bf478a9338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -316,5 +316,6 @@ constructor( val TO_LOCKSCREEN_DURATION = 1167.milliseconds val TO_AOD_DURATION = 300.milliseconds val TO_GONE_DURATION = DEFAULT_DURATION + val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3ad862b761fc..be0cf62b0526 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -251,6 +251,8 @@ constructor( * Set at 400ms for parity with [FromLockscreenTransitionInteractor] */ val DEFAULT_DURATION = 400.milliseconds + // To lockscreen duration must be at least 500ms to allow for potential screen rotation + // during the transition while the animation begins after 500ms. val TO_LOCKSCREEN_DURATION = 1.seconds val TO_BOUNCER_DURATION = 400.milliseconds val TO_OCCLUDED_DURATION = 450.milliseconds diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 75d6631008ca..77fc804d1e82 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -294,5 +294,6 @@ constructor( val TO_OCCLUDED_DURATION = 550.milliseconds val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f + val TO_DREAMING_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt index 824e0228adca..c7c54e95a63b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AccessibilityActionsViewBinder.kt @@ -19,21 +19,19 @@ package com.android.systemui.keyguard.ui.binder import android.os.Bundle import android.view.View +import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED import android.view.accessibility.AccessibilityNodeInfo import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.keyguard.ui.viewmodel.AccessibilityActionsViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlinx.coroutines.DisposableHandle -import com.android.app.tracing.coroutines.launchTraced as launch /** View binder for accessibility actions placeholder on keyguard. */ object AccessibilityActionsViewBinder { - fun bind( - view: View, - viewModel: AccessibilityActionsViewModel, - ): DisposableHandle { + fun bind(view: View, viewModel: AccessibilityActionsViewModel): DisposableHandle { val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -60,9 +58,10 @@ object AccessibilityActionsViewBinder { object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) + // Add custom actions if (canOpenGlanceableHub) { val action = @@ -80,7 +79,7 @@ object AccessibilityActionsViewBinder { override fun performAccessibilityAction( host: View, action: Int, - args: Bundle? + args: Bundle?, ): Boolean { return if ( action == R.id.accessibility_action_open_communal_hub @@ -89,6 +88,20 @@ object AccessibilityActionsViewBinder { true } else super.performAccessibilityAction(host, action, args) } + + override fun sendAccessibilityEvent( + host: View, + eventType: Int, + ) { + if (eventType == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + launch { + viewModel.clearUdfpsAccessibilityOverlayMessage( + "eventType $eventType on view $host" + ) + } + } + super.sendAccessibilityEvent(host, eventType) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index b8b032719ef8..00d41d0a7aa7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyguard.ui.binder import android.util.Log import android.view.LayoutInflater import android.view.View +import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO import android.view.ViewGroup import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_EXIT import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import androidx.constraintlayout.widget.ConstraintLayout @@ -47,6 +49,7 @@ import com.android.systemui.scrim.ScrimView import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * When necessary, adds the alternate bouncer window above most other windows (including the @@ -235,6 +238,25 @@ constructor( udfpsA11yOverlay = UdfpsAccessibilityOverlay(view.context).apply { id = udfpsA11yOverlayViewId + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + udfpsA11yOverlay.accessibilityDelegate = + object : View.AccessibilityDelegate() { + override fun sendAccessibilityEvent( + host: View, + eventType: Int, + ) { + if (eventType == TYPE_VIEW_HOVER_EXIT) { + applicationScope.launch { + udfpsA11yOverlayViewModel + .get() + .clearUdfpsAccessibilityOverlayMessage( + "$eventType on view $host" + ) + } + } + super.sendAccessibilityEvent(host, eventType) + } } view.addView(udfpsA11yOverlay) UdfpsAccessibilityOverlayBinder.bind( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt index 38f5d3e76c7c..678872d0d64d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -33,7 +34,8 @@ class AccessibilityActionsViewModel constructor( private val communalInteractor: CommunalInteractor, keyguardInteractor: KeyguardInteractor, - keyguardTransitionInteractor: KeyguardTransitionInteractor, + val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { val isCommunalAvailable = communalInteractor.isCommunalAvailable @@ -44,7 +46,7 @@ constructor( keyguardTransitionInteractor.transitionValue(KeyguardState.LOCKSCREEN).map { it == 1f }, - keyguardInteractor.statusBarState + keyguardInteractor.statusBarState, ) { transitionFinishedOnLockscreen, statusBarState -> transitionFinishedOnLockscreen && statusBarState == StatusBarState.KEYGUARD } @@ -55,4 +57,12 @@ constructor( newScene = CommunalScenes.Communal, loggingReason = "accessibility", ) + + /** + * Clears the content description to prevent the view from storing stale UDFPS directional + * guidance messages for accessibility. + */ + suspend fun clearUdfpsAccessibilityOverlayMessage(reason: String) { + udfpsOverlayInteractor.clearUdfpsAccessibilityOverlayMessage(reason) + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt new file mode 100644 index 000000000000..8771f02326fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerTransitionViewModel.kt @@ -0,0 +1,49 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition +import com.android.systemui.scene.shared.model.Overlays +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +@SysUISingleton +class DreamingToPrimaryBouncerTransitionViewModel +@Inject +constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) : + PrimaryBouncerTransition { + private val transitionAnimation = + animationFlow + .setup( + duration = FromDreamingTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, + edge = Edge.create(from = DREAMING, to = Overlays.Bouncer), + ) + .setupWithoutSceneContainer(edge = Edge.create(from = DREAMING, to = PRIMARY_BOUNCER)) + + override val windowBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = emptyFlow() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index bcbe66642d11..fd5783ef7f8e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -19,7 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.LayoutDirection import com.android.app.animation.Interpolators.EMPHASIZED import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.dagger.GlanceableHubBlurComponent import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.shared.model.Edge @@ -34,21 +37,32 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to * consume. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class GlanceableHubToLockscreenTransitionViewModel @Inject constructor( + @Application applicationScope: CoroutineScope, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, + communalSceneInteractor: CommunalSceneInteractor, + communalSettingsInteractor: CommunalSettingsInteractor, private val blurFactory: GlanceableHubBlurComponent.Factory, ) : GlanceableHubTransition, DeviceEntryIconTransition { private val transitionAnimation = @@ -59,18 +73,45 @@ constructor( ) .setupWithoutSceneContainer(edge = Edge.create(from = GLANCEABLE_HUB, to = LOCKSCREEN)) + // Whether screen rotation will happen with the transition. Only emit when idle so ongoing + // animation won't be interrupted when orientation is updated during the transition. + private val willRotateToPortraitInTransition: StateFlow<Boolean> = + if (!communalSettingsInteractor.isV2FlagEnabled()) { + flowOf(false) + } else { + communalSceneInteractor.isIdleOnCommunal.combineTransform( + communalSceneInteractor.willRotateToPortrait + ) { isIdle, willRotate -> + if (isIdle) emit(willRotate) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + override val windowBlurRadius: Flow<Float> = blurFactory.create(transitionAnimation).getBlurProvider().exitBlurRadius val keyguardAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = 167.milliseconds, - startTime = 167.milliseconds, - onStep = { it }, - onFinish = { 1f }, - onCancel = { 0f }, - name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", - ) + willRotateToPortraitInTransition.flatMapLatest { willRotate -> + transitionAnimation.sharedFlow( + duration = 167.milliseconds, + // If will rotate, start later to leave time for screen rotation. + startTime = if (willRotate) 500.milliseconds else 167.milliseconds, + onStep = { step -> + if (willRotate) { + if (!communalSceneInteractor.rotatedToPortrait.value) { + 0f + } else { + 1f + } + } else { + step + } + }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", + ) + } // Show UMO as long as keyguard is not visible. val showUmo: Flow<Boolean> = keyguardAlpha.map { alpha -> alpha == 0f } @@ -84,7 +125,14 @@ constructor( .flatMapLatest { translatePx: Int -> transitionAnimation.sharedFlowWithState( duration = TO_LOCKSCREEN_DURATION, - onStep = { value -> -translatePx + value * translatePx }, + onStep = { value -> + // do not animate translation-x if screen rotation will happen + if (willRotateToPortraitInTransition.value) { + 0f + } else { + -translatePx + value * translatePx + } + }, interpolator = EMPHASIZED, // Move notifications back to their original position since they can be // accessed from the shade, and also keyguard elements in case the animation diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt new file mode 100644 index 000000000000..9de25fcac64a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModel.kt @@ -0,0 +1,59 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition +import com.android.systemui.scene.shared.model.Overlays +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +@SysUISingleton +class PrimaryBouncerToDreamingTransitionViewModel +@Inject +constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) : + PrimaryBouncerTransition { + private val transitionAnimation = + animationFlow + .setup( + duration = FromPrimaryBouncerTransitionInteractor.TO_DREAMING_DURATION, + edge = Edge.create(from = Overlays.Bouncer, to = DREAMING), + ) + .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = DREAMING)) + + override val windowBlurRadius: Flow<Float> = + transitionAnimation.sharedFlow( + onStart = { blurConfig.maxBlurRadiusPx }, + onStep = { + transitionProgressToBlurRadius( + blurConfig.maxBlurRadiusPx, + endBlurRadius = blurConfig.minBlurRadiusPx, + transitionProgress = it, + ) + }, + onFinish = { blurConfig.minBlurRadiusPx }, + ) + + override val notificationBlurRadius: Flow<Float> = emptyFlow() +} diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 465c78e91e53..2a7fb5467173 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -23,17 +23,14 @@ import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf @@ -51,31 +48,12 @@ constructor( val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, - shadeModeInteractor: ShadeModeInteractor, disableFlagsInteractor: DisableFlagsInteractor, mediaCarouselInteractor: MediaCarouselInteractor, - activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator") - val showClock: Boolean by - hydrator.hydratedStateOf( - traceName = "showClock", - initialValue = - shouldShowClock( - isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value, - areAnyNotificationsPresent = - activeNotificationsInteractor.areAnyNotificationsPresentValue, - ), - source = - combine( - shadeModeInteractor.isShadeLayoutWide, - activeNotificationsInteractor.areAnyNotificationsPresent, - this::shouldShowClock, - ), - ) - val showMedia: Boolean by hydrator.hydratedStateOf( traceName = "showMedia", @@ -114,13 +92,6 @@ constructor( shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } - private fun shouldShowClock( - isShadeLayoutWide: Boolean, - areAnyNotificationsPresent: Boolean, - ): Boolean { - return !isShadeLayoutWide && areAnyNotificationsPresent - } - @AssistedFactory interface Factory { fun create(): NotificationsShadeOverlayContentViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index f8eaa6c3bcfb..b8cb2c4844e4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -22,8 +22,11 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -108,6 +111,7 @@ import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.contentDescription @@ -118,6 +122,7 @@ import androidx.compose.ui.text.style.Hyphens import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory @@ -157,9 +162,9 @@ import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell -import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.shared.model.TileCategory import com.android.systemui.qs.shared.model.groupAndSort import com.android.systemui.res.R import kotlin.math.abs @@ -220,7 +225,6 @@ private fun EditModeTopBar(onStopEditing: () -> Unit, onReset: (() -> Unit)?) { ) } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DefaultEditTileGrid( listState: EditTileListState, @@ -526,11 +530,7 @@ private fun CurrentTilesGrid( var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) } val coroutineScope = rememberCoroutineScope() - val cells = - remember(listState.tiles) { - listState.tiles.fastMap { Pair(it, BounceableTileViewModel()) } - } - + val cells = listState.tiles val primaryColor = MaterialTheme.colorScheme.primary TileLazyGrid( state = gridState, @@ -561,11 +561,11 @@ private fun CurrentTilesGrid( .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { EditTiles( - cells, - listState, - selectionState, - coroutineScope, - largeTilesSpan, + cells = cells, + dragAndDropState = listState, + selectionState = selectionState, + coroutineScope = coroutineScope, + largeTilesSpan = largeTilesSpan, onRemoveTile = onRemoveTile, ) { resizingOperation -> when (resizingOperation) { @@ -618,11 +618,9 @@ private fun AvailableTileGrid( } .padding(16.dp), ) { - Text( - text = category.label.load() ?: "", - style = MaterialTheme.typography.titleMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth().padding(start = 8.dp, bottom = 16.dp), + CategoryHeader( + category, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), ) tiles.chunked(columns).forEach { row -> Row( @@ -662,7 +660,7 @@ private fun GridCell.key(index: Int): Any { /** * Adds a list of [GridCell] to the lazy grid * - * @param cells the pairs of [GridCell] to [BounceableTileViewModel] + * @param cells the list of [GridCell] * @param dragAndDropState the [DragAndDropState] for this grid * @param selectionState the [MutableSelectionState] for this grid * @param coroutineScope the [CoroutineScope] to be used for the tiles @@ -671,7 +669,7 @@ private fun GridCell.key(index: Int): Any { * @param onResize the callback when a tile has a new [ResizeOperation] */ fun LazyGridScope.EditTiles( - cells: List<Pair<GridCell, BounceableTileViewModel>>, + cells: List<GridCell>, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, coroutineScope: CoroutineScope, @@ -681,11 +679,11 @@ fun LazyGridScope.EditTiles( ) { items( count = cells.size, - key = { cells[it].first.key(it) }, - span = { cells[it].first.span }, + key = { cells[it].key(it) }, + span = { cells[it].span }, contentType = { TileType }, ) { index -> - when (val cell = cells[index].first) { + when (val cell = cells[index]) { is TileGridCell -> if (dragAndDropState.isMoving(cell.tile.tileSpec)) { // If the tile is being moved, replace it with a visible spacer @@ -708,7 +706,15 @@ fun LazyGridScope.EditTiles( onRemoveTile = onRemoveTile, coroutineScope = coroutineScope, largeTilesSpan = largeTilesSpan, - modifier = Modifier.animateItem(), + modifier = + Modifier.animateItem( + placementSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioLowBouncy, + visibilityThreshold = IntOffset.VisibilityThreshold, + ) + ), ) } is SpacerGridCell -> @@ -853,6 +859,26 @@ private fun TileGridCell( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable +private fun CategoryHeader(category: TileCategory, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + modifier = modifier, + ) { + Icon( + painter = painterResource(category.iconId), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = category.label.load() ?: "", + style = MaterialTheme.typography.titleMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface, + ) + } +} + +@Composable private fun AvailableTileGridCell( cell: AvailableTileGridCell, dragAndDropState: DragAndDropState, diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt index 59cb7d3d5345..c8225e7a3509 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt @@ -20,14 +20,35 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.res.R /** Categories for tiles. This can be used to sort tiles in edit mode. */ -enum class TileCategory(val label: Text) { - CONNECTIVITY(Text.Resource(R.string.qs_edit_mode_category_connectivity)), - UTILITIES(Text.Resource(R.string.qs_edit_mode_category_utilities)), - DISPLAY(Text.Resource(R.string.qs_edit_mode_category_display)), - PRIVACY(Text.Resource(R.string.qs_edit_mode_category_privacy)), - ACCESSIBILITY(Text.Resource(R.string.qs_edit_mode_category_accessibility)), - PROVIDED_BY_APP(Text.Resource(R.string.qs_edit_mode_category_providedByApps)), - UNKNOWN(Text.Resource(R.string.qs_edit_mode_category_unknown)), +enum class TileCategory(val label: Text, val iconId: Int) { + CONNECTIVITY( + Text.Resource(R.string.qs_edit_mode_category_connectivity), + R.drawable.ic_qs_category_connectivty, + ), + UTILITIES( + Text.Resource(R.string.qs_edit_mode_category_utilities), + R.drawable.ic_qs_category_utilities, + ), + DISPLAY( + Text.Resource(R.string.qs_edit_mode_category_display), + R.drawable.ic_qs_category_display, + ), + PRIVACY( + Text.Resource(R.string.qs_edit_mode_category_privacy), + R.drawable.ic_qs_category_privacy, + ), + ACCESSIBILITY( + Text.Resource(R.string.qs_edit_mode_category_accessibility), + R.drawable.ic_qs_category_accessibility, + ), + PROVIDED_BY_APP( + Text.Resource(R.string.qs_edit_mode_category_providedByApps), + R.drawable.ic_qs_category_provided_by_apps, + ), + UNKNOWN( + Text.Resource(R.string.qs_edit_mode_category_unknown), + R.drawable.ic_qs_category_unknown, + ), } interface CategoryAndName { diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index 73c71f6088e1..452ea3f719fa 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -90,7 +90,7 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: fun logSceneChangeRejection( from: ContentKey?, to: ContentKey?, - originalChangeReason: String, + originalChangeReason: String?, rejectionReason: String, ) { logBuffer.log( @@ -112,8 +112,10 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: "scene " } ) - append("change $str1 because \"$str2\" ") - append("(original change reason: \"$str3\")") + append("change $str1 because \"$str2\"") + if (str3 != null) { + append(" (original change reason: \"$str3\")") + } } }, ) @@ -136,8 +138,11 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: logBuffer.log( tag = TAG, level = LogLevel.INFO, - messageInitializer = { str1 = transitionState.currentScene.toString() }, - messagePrinter = { "Scene transition idle on: $str1" }, + messageInitializer = { + str1 = transitionState.currentScene.toString() + str2 = transitionState.currentOverlays.joinToString() + }, + messagePrinter = { "Scene transition idle on: $str1, overlays: $str2" }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index a81fcec94989..d8bb84af6023 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -219,15 +219,24 @@ constructor( * it being a false touch. */ fun canChangeScene(toScene: SceneKey): Boolean { - return isInteractionAllowedByFalsing(toScene).also { - // A scene change is guaranteed; log it. - logger.logSceneChanged( - from = currentScene.value, - to = toScene, - sceneState = null, - reason = "user interaction", - isInstant = false, - ) + return isInteractionAllowedByFalsing(toScene).also { sceneChangeAllowed -> + if (sceneChangeAllowed) { + // A scene change is guaranteed; log it. + logger.logSceneChanged( + from = currentScene.value, + to = toScene, + sceneState = null, + reason = "user interaction", + isInstant = false, + ) + } else { + logger.logSceneChangeRejection( + from = currentScene.value, + to = toScene, + originalChangeReason = null, + rejectionReason = "Falsing: false touch detected", + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index c800ab3d0bf2..913aacb53e12 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -20,7 +20,6 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Rect import android.os.PowerManager -import android.os.SystemClock import android.util.ArraySet import android.view.GestureDetector import android.view.MotionEvent @@ -54,6 +53,7 @@ import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.communal.util.UserTouchActivityNotifier import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -101,6 +101,7 @@ constructor( private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val keyguardMediaController: KeyguardMediaController, private val lockscreenSmartspaceController: LockscreenSmartspaceController, + private val userTouchActivityNotifier: UserTouchActivityNotifier, @CommunalTouchLog logBuffer: LogBuffer, private val userActivityNotifier: UserActivityNotifier, ) : LifecycleOwner { @@ -646,8 +647,8 @@ constructor( // result in broken states. return true } + var handled = hubShowing try { - var handled = false if (!touchTakenByKeyguardGesture) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { @@ -655,18 +656,10 @@ constructor( } } } - return handled || hubShowing + return handled } finally { - if (Flags.bouncerUiRevamp()) { - userActivityNotifier.notifyUserActivity( - event = PowerManager.USER_ACTIVITY_EVENT_TOUCH - ) - } else { - powerManager.userActivity( - SystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, - 0, - ) + if (handled) { + userTouchActivityNotifier.notifyActivity(ev) } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS index 89454b84a528..47ca531b8502 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS @@ -1,5 +1,4 @@ justinweir@google.com -syeonlee@google.com nicomazz@google.com burakov@google.com @@ -8,16 +7,16 @@ per-file *Notification* = file:../statusbar/notification/OWNERS per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com -per-file *ShadeHeader* = syeonlee@google.com, kozynski@google.com, asc@google.com +per-file *ShadeHeader* = kozynski@google.com, asc@google.com per-file *Interactor* = set noparent -per-file *Interactor* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com +per-file *Interactor* = justinweir@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com per-file *Repository* = set noparent -per-file *Repository* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com +per-file *Repository* = justinweir@google.com, nijamkin@google.com, nicomazz@google.com, burakov@google.com -per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com +per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com -per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com -per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com, nicomazz@google.com, burakov@google.com +per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com +per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, nicomazz@google.com, burakov@google.com diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 446d4b450edc..0132390f9ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -23,6 +23,7 @@ import android.view.WindowManager import android.view.WindowManager.LayoutParams import android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE import android.window.WindowContext +import com.android.app.tracing.TrackGroupUtils.trackGroup import com.android.systemui.CoreStartable import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.ConfigurationStateImpl @@ -34,6 +35,8 @@ import com.android.systemui.common.ui.view.ChoreographerUtils import com.android.systemui.common.ui.view.ChoreographerUtilsImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogBufferFactory import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository @@ -266,6 +269,20 @@ object ShadeDisplayAwareModule { @Provides @ShadeOnDefaultDisplayWhenLocked fun provideShadeOnDefaultDisplayWhenLocked(): Boolean = true + + /** Provides a [LogBuffer] for use by classes related to shade movement */ + @Provides + @SysUISingleton + @ShadeDisplayLog + fun provideShadeDisplayLogLogBuffer(factory: LogBufferFactory): LogBuffer { + val logBufferName = "ShadeDisplayLog" + return factory.create( + logBufferName, + maxSize = 400, + alwaysLogToLogcat = true, + systraceTrackName = trackGroup("shade", logBufferName), + ) + } } /** Module that should be included only if the shade window [WindowRootView] is available. */ @@ -298,3 +315,6 @@ object ShadeDisplayAwareWithShadeWindowModule { * how well this solution behaves from the performance point of view. */ @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeOnDefaultDisplayWhenLocked + +/** A [com.android.systemui.log.LogBuffer] for changes to the shade display. */ +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayLog diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt index de1b180f5a7a..1ec83835ab43 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt @@ -24,6 +24,8 @@ import com.android.systemui.CoreStartable import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeModeInteractor @@ -42,6 +44,7 @@ constructor( private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>, @ShadeDisplayAware private val configurationRepository: ConfigurationRepository, @Application private val scope: CoroutineScope, + @ShadeDisplayLog private val logBuffer: LogBuffer, ) : CoreStartable { override fun start() { scope.launchTraced("ShadeStateTraceLogger") { @@ -72,6 +75,18 @@ constructor( "configurationChange#smallestScreenWidthDp", it.smallestScreenWidthDp, ) + logBuffer.log( + "ShadeStateTraceLogger", + LogLevel.DEBUG, + { + int1 = it.smallestScreenWidthDp + int2 = it.densityDpi + }, + { + "New configuration change from Shade window. " + + "smallestScreenWidthDp: $int1, densityDpi: $int2" + }, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index 0e0f58dc8d0e..d48d56c2403b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -27,8 +27,11 @@ import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker +import com.android.systemui.shade.ShadeDisplayLog import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo import com.android.systemui.shade.ShadeTraceLogger.t import com.android.systemui.shade.ShadeTraceLogger.traceReparenting @@ -69,6 +72,7 @@ constructor( private val notificationRebindingTracker: NotificationRebindingTracker, private val notificationStackRebindingHider: NotificationStackRebindingHider, @ShadeDisplayAware private val configForwarder: ConfigurationForwarder, + @ShadeDisplayLog private val logBuffer: LogBuffer, ) : CoreStartable { private val hasActiveNotifications: Boolean @@ -101,7 +105,12 @@ constructor( /** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */ private suspend fun moveShadeWindowTo(destinationId: Int) { - Log.d(TAG, "Trying to move shade window to display with id $destinationId") + logBuffer.log( + TAG, + LogLevel.DEBUG, + { int1 = destinationId }, + { "Trying to move shade window to display with id $int1" }, + ) logMoveShadeWindowTo(destinationId) var currentId = -1 try { @@ -113,7 +122,12 @@ constructor( val currentDisplay = shadeContext.display ?: error("Current shade display is null") currentId = currentDisplay.displayId if (currentId == destinationId) { - Log.w(TAG, "Trying to move the shade to a display ($currentId) it was already in ") + logBuffer.log( + TAG, + LogLevel.WARNING, + { int1 = currentId }, + { "Trying to move the shade to a display ($int1) it was already in." }, + ) return } @@ -128,9 +142,14 @@ constructor( } } } catch (e: IllegalStateException) { - Log.e( + logBuffer.log( TAG, - "Unable to move the shade window from display $currentId to $destinationId", + LogLevel.ERROR, + { + int1 = currentId + int2 = destinationId + }, + { "Unable to move the shade window from display $int1 to $int2" }, e, ) } @@ -200,7 +219,7 @@ constructor( } private fun errorLog(s: String) { - Log.e(TAG, s) + logBuffer.log(TAG, LogLevel.ERROR, s) } private fun checkContextDisplayMatchesExpected(destinationId: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt index 6431f303089f..5b989d8e1e7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt @@ -16,15 +16,18 @@ package com.android.systemui.statusbar.chips.notification.shared -import com.android.systemui.Flags +import android.app.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils +// NOTE: We're merging this flag with the `ui_rich_ongoing` flag. +// We'll replace all usages of this class with PromotedNotificationUi as a follow-up. + /** Helper for reading or using the status bar promoted notification chips flag state. */ @Suppress("NOTHING_TO_INLINE") object StatusBarNotifChips { /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_NOTIFICATION_CHIPS + const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING /** A token used for dependency declaration */ val token: FlagToken @@ -33,7 +36,7 @@ object StatusBarNotifChips { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.statusBarNotificationChips() + get() = Flags.uiRichOngoing() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index fa8d25623d67..18cecb4abc31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -132,7 +132,11 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = } is OngoingActivityChipModel.Active.ShortTimeDelta -> { - val timeRemainingState = rememberTimeRemainingState(futureTimeMillis = viewModel.time) + val timeRemainingState = + rememberTimeRemainingState( + futureTimeMillis = viewModel.time, + timeSource = viewModel.timeSource, + ) timeRemainingState.timeRemainingData?.let { val text = formatTimeRemainingData(it) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index d7b67b1f7bfb..2c4746f5fafb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -136,7 +136,8 @@ sealed class OngoingActivityChipModel { /** * The [TimeSource] that should be used to track the current time for this timer. Should - * be compatible with [startTimeMs]. + * be compatible units with [startTimeMs]. Only used in the Compose version of the + * chips. */ val timeSource: TimeSource = TimeSource { SystemClock.elapsedRealtime() }, @@ -187,6 +188,12 @@ sealed class OngoingActivityChipModel { * this model and the [Timer] model use the same units. */ @CurrentTimeMillisLong val time: Long, + + /** + * The [TimeSource] that should be used to track the current time for this timer. Should + * be compatible units with [time]. Only used in the Compose version of the chips. + */ + val timeSource: TimeSource = TimeSource { System.currentTimeMillis() }, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val transitionManager: TransitionManager? = null, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt index 803d422c0f0f..2d2d13ce6c27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt @@ -100,10 +100,7 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT /** Remember and manage the TimeRemainingState */ @Composable -fun rememberTimeRemainingState( - futureTimeMillis: Long, - timeSource: TimeSource = remember { TimeSource { System.currentTimeMillis() } }, -): TimeRemainingState { +fun rememberTimeRemainingState(futureTimeMillis: Long, timeSource: TimeSource): TimeRemainingState { val state = remember(timeSource, futureTimeMillis) { TimeRemainingState(timeSource, futureTimeMillis) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 6b32c6a18ec0..a0b3c1729154 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -298,6 +298,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mRowContentBindStage.requestRebind(entry, en -> { mLogger.logRebindComplete(entry); row.setIsMinimized(isMinimized); + row.setRedactionType(redactionType); if (inflationCallback != null) { inflationCallback.onAsyncInflationFinished(en); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt index c6e3da1c5750..69e27dcc2e6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt @@ -16,14 +16,17 @@ package com.android.systemui.statusbar.notification.promoted -import com.android.systemui.Flags +import android.app.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils +// NOTE: We're merging this flag with the `ui_rich_ongoing` flag. +// We'll replace all usages of this class with PromotedNotificationUi as a follow-up. + /** Helper for reading or using the promoted ongoing notifications AOD flag state. */ object PromotedNotificationUiAod { /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_AOD_UI_RICH_ONGOING + const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING /** A token used for dependency declaration */ val token: FlagToken @@ -32,7 +35,7 @@ object PromotedNotificationUiAod { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.aodUiRichOngoing() + get() = Flags.uiRichOngoing() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt index adeddde8ccc3..5c0991059dec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt @@ -16,15 +16,18 @@ package com.android.systemui.statusbar.notification.promoted -import com.android.systemui.Flags +import android.app.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils +// NOTE: We're merging this flag with the `ui_rich_ongoing` flag. +// We'll replace all usages of this class with PromotedNotificationUi as a follow-up. + /** Helper for reading or using the expanded ui rich ongoing flag state. */ @Suppress("NOTHING_TO_INLINE") object PromotedNotificationUiForceExpanded { /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING_FORCE_EXPANDED + const val FLAG_NAME = Flags.FLAG_UI_RICH_ONGOING /** A token used for dependency declaration */ val token: FlagToken @@ -33,7 +36,7 @@ object PromotedNotificationUiForceExpanded { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.uiRichOngoingForceExpanded() + get() = Flags.uiRichOngoing() /** * Called to ensure code is only run when the flag is enabled. This protects users from the 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 d9778bdde0a5..fa9a7b9b524e 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,10 +18,13 @@ 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.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.policy.domain.interactor.SensitiveNotificationProtectionInteractor import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -30,14 +33,31 @@ class AODPromotedNotificationInteractor @Inject constructor( promotedNotificationsInteractor: PromotedNotificationsInteractor, + keyguardInteractor: KeyguardInteractor, + sensitiveNotificationProtectionInteractor: SensitiveNotificationProtectionInteractor, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { + + /** + * Whether the system is unlocked and not screensharing such that private notification content + * is allowed to show on the aod + */ + private val canShowPrivateNotificationContent: Flow<Boolean> = + combine( + keyguardInteractor.isKeyguardDismissible, + sensitiveNotificationProtectionInteractor.isSensitiveStateActive, + ) { isKeyguardDismissible, isSensitive -> + isKeyguardDismissible && !isSensitive + } + /** The content to show as the promoted notification on AOD */ val content: Flow<PromotedNotificationContentModel?> = - promotedNotificationsInteractor.aodPromotedNotification - .map { - // TODO(b/400991304): show the private version when unlocked - it?.publicVersion + combine( + promotedNotificationsInteractor.aodPromotedNotification, + canShowPrivateNotificationContent, + ) { promotedContent, showPrivateContent -> + if (showPrivateContent) promotedContent?.privateVersion + else promotedContent?.publicVersion } .distinctUntilNewInstance() 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 bef3c691cb4d..740391d7010e 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 @@ -26,6 +26,7 @@ import static com.android.systemui.Flags.notificationRowAccessibilityExpanded; import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.Flags.notificationsPinnedHunInShade; import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; @@ -102,6 +103,7 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; @@ -503,7 +505,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private final ListenerSet<DismissButtonTargetVisibilityListener> mDismissButtonTargetVisibilityListeners = new ListenerSet<>(); - + @RedactionType + private int mRedactionType = REDACTION_TYPE_NONE; public NotificationContentView[] getLayouts() { return Arrays.copyOf(mLayouts, mLayouts.length); } @@ -1867,6 +1870,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * Set the redaction type of the row. + */ + public void setRedactionType(@RedactionType int redactionType) { + mRedactionType = redactionType; + } + + /** * Init the bundle header view. The ComposeView is initialized within with the passed viewModel. * This can only be init once and not in conjunction with any other header view. */ @@ -4516,6 +4526,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", expandedWhenPinned: " + mExpandedWhenPinned); pw.print(", isMinimized: " + mIsMinimized); pw.print(", isAboveShelf: " + isAboveShelf()); + pw.print(", redactionType: " + mRedactionType); pw.println(); if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 488aa44ddd3b..756a2c19c10e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1248,7 +1248,7 @@ public class NotificationContentView extends FrameLayout implements Notification final boolean isSingleLineViewPresent = mSingleLineView != null; if (shouldShowSingleLineView && !isSingleLineViewPresent) { - Log.e(TAG, "calculateVisibleType: SingleLineView is not available!"); + Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!"); } final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent @@ -1274,9 +1274,6 @@ public class NotificationContentView extends FrameLayout implements Notification } final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded(); final boolean isSingleLinePresent = mSingleLineView != null; - if (shouldShowSingleLineView && !isSingleLinePresent) { - Log.e(TAG, "getVisualTypeForHeight: singleLineView is not available."); - } if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) { return VISIBLE_TYPE_SINGLELINE; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 2cf3b14bb8c5..0257b4c2397e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -218,7 +218,7 @@ public class NotificationConversationInfo extends LinearLayout implements @Background Handler bgHandler, OnConversationSettingsClickListener onConversationSettingsClickListener, Optional<BubblesManager> bubblesManagerOptional, - ShadeController shadeController) { + ShadeController shadeController, boolean isDismissable, OnClickListener onCloseClick) { mINotificationManager = iNotificationManager; mPeopleSpaceWidgetManager = peopleSpaceWidgetManager; mOnUserInteractionCallback = onUserInteractionCallback; @@ -263,6 +263,11 @@ public class NotificationConversationInfo extends LinearLayout implements bindHeader(); bindActions(); + View dismissButton = findViewById(R.id.inline_dismiss); + dismissButton.setOnClickListener(onCloseClick); + dismissButton.setVisibility(dismissButton.hasOnClickListeners() && isDismissable + ? VISIBLE : GONE); + View done = findViewById(R.id.done); done.setOnClickListener(mOnDone); done.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 6c7c7a79348f..d0567f08c2f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -608,7 +608,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta mBgHandler, onConversationSettingsListener, mBubblesManagerOptional, - mShadeController); + mShadeController, + row.canViewBeDismissed(), + row.getCloseButtonOnClickListener(row)); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java index 19321dcef5c7..d5e2e7eb3a9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java @@ -43,7 +43,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ContrastColorUtil; import com.android.internal.widget.NotificationActionListLayout; import com.android.systemui.Dependency; -import com.android.systemui.Flags; import com.android.systemui.UiOffloadThread; import com.android.systemui.res.R; import com.android.systemui.statusbar.CrossFadeHelper; @@ -51,6 +50,7 @@ import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; import com.android.systemui.statusbar.notification.ImageTransformState; import com.android.systemui.statusbar.notification.TransformState; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.HybridNotificationView; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; @@ -196,7 +196,8 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp } private void adjustTitleAndRightIconForPromotedOngoing() { - if (Flags.uiRichOngoingForceExpanded() && mRow.isPromotedOngoing() && mRightIcon != null) { + if (PromotedNotificationUiForceExpanded.isEnabled() && + mRow.isPromotedOngoing() && mRightIcon != null) { final int horizontalMargin; if (notificationsRedesignTemplates()) { horizontalMargin = mView.getResources().getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 10821dffd394..1f4ccd59b063 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -84,7 +84,7 @@ interface MobileIconsInteractor { val icons: StateFlow<List<MobileIconInteractor>> /** Whether the mobile icons can be stacked vertically. */ - val isStackable: StateFlow<Boolean> + val isStackable: Flow<Boolean> /** * Observable for the subscriptionId of the current mobile data connection. Null if we don't @@ -309,21 +309,20 @@ constructor( override val isStackable = if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { - icons.flatMapLatest { icons -> - combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> - // These are only stackable if: - // - They are cellular - // - There's exactly two - // - They have the same number of levels - signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { - it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels - } + icons.flatMapLatest { icons -> + combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> + // These are only stackable if: + // - They are cellular + // - There's exactly two + // - They have the same number of levels + signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { + it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels } } - } else { - flowOf(false) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + } else { + flowOf(false) + } /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt index 54cd8e3c46e4..72ff3b67c317 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt @@ -18,9 +18,11 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags import com.android.systemui.kairos.ExperimentalKairosApi @@ -48,7 +50,7 @@ object StackedMobileIconBinder { return SingleBindableStatusBarComposeIconView.withDefaultBinding( view = view, shouldBeVisible = { mobileIconsViewModel.isStackable.value }, - ) { _, tint -> + ) { _, tintFlow -> view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { view.composeView.apply { @@ -66,8 +68,9 @@ object StackedMobileIconBinder { viewModelFactory.create() } } + val tint by tintFlow.collectAsStateWithLifecycle() if (viewModel.isIconVisible) { - CompositionLocalProvider(LocalContentColor provides Color(tint())) { + CompositionLocalProvider(LocalContentColor provides Color(tint)) { StackedMobileIcon(viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 494d95e7f177..997b185fdee5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -99,7 +101,17 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val isStackable: StateFlow<Boolean> = interactor.isStackable + /** Whether all of [mobileSubViewModels] are visible or not. */ + private val iconsAreAllVisible = + mobileSubViewModels.flatMapLatest { viewModels -> + combine(viewModels.map { it.isVisible }) { isVisibleArray -> isVisibleArray.all { it } } + } + + val isStackable: StateFlow<Boolean> = + combine(iconsAreAllVisible, interactor.isStackable) { isVisible, isStackable -> + isVisible && isStackable + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) init { scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt index 8076040564fb..9d1df8967fc0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** Compose view that is bound to bindable_status_bar_compose_icon.xml */ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeSet?) : @@ -78,7 +79,7 @@ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeS fun withDefaultBinding( view: SingleBindableStatusBarComposeIconView, shouldBeVisible: () -> Boolean, - block: suspend LifecycleOwner.(View, () -> Int) -> Unit, + block: suspend LifecycleOwner.(View, StateFlow<Int>) -> Unit, ): ModernStatusBarViewBinding { @StatusBarIconView.VisibleState val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) @@ -90,7 +91,7 @@ class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeS view.repeatWhenAttached { // Child binding - block(view) { iconTint.value } + block(view, iconTint) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index b33c2005479e..4e18935834cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -34,6 +34,10 @@ import android.graphics.drawable.Icon import android.os.Build import android.os.Bundle import android.os.SystemClock +import android.text.Annotation +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan import android.util.Log import android.view.ContextThemeWrapper import android.view.LayoutInflater @@ -504,15 +508,40 @@ constructor( choice: CharSequence, delayOnClickListener: Boolean, ): Button { - val layoutRes = + val enableAnimatedReply = Flags.notificationAnimatedActionsTreatment() && + smartReplies.fromAssistant && isAnimatedReply(choice) + val layoutRes = if (enableAnimatedReply) { + R.layout.animated_action_button + } else { if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button else R.layout.smart_reply_button + } + return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button) .apply { - text = choice + // choiceToDeliver does not contain Annotation with extra data + val choiceToDeliver: CharSequence + if (enableAnimatedReply) { + choiceToDeliver = choice.toString() + // If the choice is animated reply, format the text by concatenating + // attributionText with different color to choice text + val fullTextWithAttribution = formatChoiceWithAttribution(choice) + text = fullTextWithAttribution + } else { + choiceToDeliver = choice + text = choice + } + val onClickListener = View.OnClickListener { - onSmartReplyClick(entry, smartReplies, replyIndex, parent, this, choice) + onSmartReplyClick( + entry, + smartReplies, + replyIndex, + parent, + this, + choiceToDeliver + ) } setOnClickListener( if (delayOnClickListener) @@ -600,6 +629,47 @@ constructor( RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE) return intent } + + // Check if the choice is animated reply + private fun isAnimatedReply(choice: CharSequence): Boolean { + if (choice is Spanned) { + val annotations = choice.getSpans(0, choice.length, Annotation::class.java) + for (annotation in annotations) { + if (annotation.key == "isAnimatedReply" && annotation.value == "1") { + return true + } + } + } + return false + } + + // Format the text by concatenating attributionText with attribution text color to choice text + private fun formatChoiceWithAttribution(choice: CharSequence): CharSequence { + val colorInt = context.getColor(R.color.animated_action_button_attribution_color) + if (choice is Spanned) { + val annotations = choice.getSpans(0, choice.length, Annotation::class.java) + for (annotation in annotations) { + if (annotation.key == "attributionText") { + // Extract the attribution text + val extraText = annotation.value + // Concatenate choice text and attribution text + val spannableWithColor = SpannableStringBuilder(choice) + spannableWithColor.append(" $extraText") + // Apply color to attribution text + spannableWithColor.setSpan( + ForegroundColorSpan(colorInt), + choice.length, + spannableWithColor.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + return spannableWithColor + } + } + } + + // Return the original if no attributionText found + return choice.toString() + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt new file mode 100644 index 000000000000..0a6a4c2e44e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractor.kt @@ -0,0 +1,48 @@ +/* + * 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.policy.domain.interactor + +import com.android.server.notification.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf + +/** A interactor which provides the current sensitive notification protections status */ +@SysUISingleton +class SensitiveNotificationProtectionInteractor +@Inject +constructor(private val controller: SensitiveNotificationProtectionController) { + + /** sensitive notification protections status */ + val isSensitiveStateActive: Flow<Boolean> = + if (Flags.screenshareNotificationHiding()) { + conflatedCallbackFlow { + val listener = Runnable { trySend(controller.isSensitiveStateActive) } + controller.registerSensitiveStateListener(listener) + trySend(controller.isSensitiveStateActive) + awaitClose { controller.unregisterSensitiveStateListener(listener) } + } + .distinctUntilChanged() + } else { + flowOf(false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index bf79d11b2fb8..515a10792c02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -1482,6 +1482,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } else { assertThat(hint.isNullOrBlank()).isTrue() } + + kosmos.promptViewModel.onClearUdfpsGuidanceHint(true) + + if (testCase.modalities.hasUdfps) { + assertThat(hint).isNull() + } else { + assertThat(hint.isNullOrBlank()).isTrue() + } } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index 2898a02a1da8..cd6757c6e7ea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -38,6 +38,7 @@ import android.content.res.Resources; import android.graphics.Color; import android.media.AudioManager; import android.os.Handler; +import android.os.PowerManager; import android.os.UserManager; import android.provider.Settings; import android.testing.TestableLooper; @@ -142,6 +143,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock private UserLogoutInteractor mLogoutInteractor; @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher; + @Mock private PowerManager mPowerManager; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; private TestableLooper mTestableLooper; @@ -204,7 +206,8 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mSelectedUserInteractor, mLogoutInteractor, mInteractor, - () -> new FakeDisplayWindowPropertiesRepository(mContext) + () -> new FakeDisplayWindowPropertiesRepository(mContext), + mPowerManager ); mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting(); @@ -806,6 +809,101 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { assertThat(mGlobalActionsDialogLite.mDialog).isNull(); } + @Test + public void testShouldLogStandbyPress() { + GlobalActionsDialogLite.StandbyAction standbyAction = + mGlobalActionsDialogLite.new StandbyAction(); + standbyAction.onPress(); + verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_STANDBY_PRESS); + } + + @Test + public void testCreateActionItems_standbyEnabled_doesShowStandby() { + // Test like a TV, which only has standby and shut down + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + mGlobalActionsDialogLite.createActionItems(); + + assertItemsOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class, + GlobalActionsDialogLite.ShutDownAction.class); + assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty(); + assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty(); + } + + @Test + public void testCreateActionItems_standbyDisabled_doesntStandbyAction() { + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(5).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayEmergency(); + doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any()); + doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any()); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + mGlobalActionsDialogLite.createActionItems(); + + assertNoItemsOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class); + assertThat(mGlobalActionsDialogLite.mOverflowItems).isEmpty(); + assertThat(mGlobalActionsDialogLite.mPowerItems).isEmpty(); + } + + @Test + public void testCreateActionItems_standbyEnabled_locked_showsStandby() { + // Test like a TV, which only has standby and shut down + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + + // Show dialog with keyguard showing and provisioned + mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); + + assertOneItemOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class); + + // Hide dialog + mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY); + } + + @Test + public void testCreateActionItems_standbyEnabled_notProvisioned_showsStandby() { + // Test like a TV, which only has standby and shut down. + mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); + doReturn(2).when(mGlobalActionsDialogLite).getMaxShownPowerItems(); + String[] actions = { + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_STANDBY, + GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER + }; + doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions(); + + // Show dialog without keyguard showing and not provisioned + mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); + // Clear the dismiss override so we don't have behavior after dismissing the dialog + mGlobalActionsDialogLite.mDialog.setDismissOverride(null); + + assertOneItemOfType(mGlobalActionsDialogLite.mItems, + GlobalActionsDialogLite.StandbyAction.class); + + // Hide dialog + mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY); + } + private UserInfo mockCurrentUser(int flags) { return new UserInfo(10, "A User", flags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 7728f684f0f2..c21570928bde 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.communal.util.userTouchActivityNotifier import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -64,6 +65,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.controller.keyguardMediaController +import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -137,6 +139,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -178,6 +181,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -208,6 +212,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -234,6 +239,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, + userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) @@ -539,6 +545,18 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun onTouchEvent_touchHandled_notifyUserActivity() = + kosmos.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Touch event is sent to the container view. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + verify(containerView).onTouchEvent(DOWN_EVENT) + assertThat(fakePowerRepository.userTouchRegistered).isTrue() + } + + @Test fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() = kosmos.runTest { // Communal is open. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java index 2c800bd87ef5..a515c3f6ed6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java @@ -171,6 +171,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { private ConversationIconFactory mIconFactory; @Mock private Notification.BubbleMetadata mBubbleMetadata; + @Mock + private View.OnClickListener mCloseListener; private Handler mTestHandler; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -298,7 +300,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, mCloseListener); } @Test @@ -402,7 +404,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); assertEquals(VISIBLE, nameView.getVisibility()); assertTrue(nameView.getText().toString().contains("Proxied")); @@ -442,7 +444,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final View feedback = mNotificationInfo.findViewById(R.id.feedback); assertEquals(VISIBLE, feedback.getVisibility()); @@ -484,7 +486,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); settingsButton.performClick(); @@ -524,7 +526,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { false, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertTrue(settingsButton.getVisibility() != View.VISIBLE); } @@ -601,7 +603,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); assertThat(((TextView) mNotificationInfo.findViewById(R.id.priority_summary)).getText()) .isEqualTo(mContext.getString( R.string.notification_channel_summary_priority_dnd)); @@ -633,7 +635,7 @@ public class NotificationConversationInfoTest extends SysuiTestCase { true, mTestHandler, mTestHandler, null, Optional.of(mBubblesManager), - mShadeController); + mShadeController, true, null); assertThat(((TextView) mNotificationInfo.findViewById(R.id.priority_summary)).getText()) .isEqualTo(mContext.getString( R.string.notification_channel_summary_priority_baseline)); @@ -1018,4 +1020,19 @@ public class NotificationConversationInfoTest extends SysuiTestCase { // THEN the user is not presented with the People Tile pinning request verify(mPeopleSpaceWidgetManager, never()).requestPinAppWidget(eq(mShortcutInfo), any()); } + + + @Test + public void testDismiss() throws Exception { + doStandardBind(); + + View dismiss = mNotificationInfo.findViewById(R.id.inline_dismiss); + dismiss.performClick(); + mTestableLooper.processAllMessages(); + + // Verify action performed on button click + verify(mCloseListener).onClick(any()); + + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt index 3f35bb9f3520..38e6c8a0cdea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.data.repository +import android.content.res.Configuration import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn @@ -48,4 +50,13 @@ class FakeCommunalSceneRepository( override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } + + private val _communalContainerOrientation = + MutableStateFlow(Configuration.ORIENTATION_UNDEFINED) + override val communalContainerOrientation: StateFlow<Int> = + _communalContainerOrientation.asStateFlow() + + override fun setCommunalContainerOrientation(orientation: Int) { + _communalContainerOrientation.value = orientation + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt index 209d1636e380..8834af581e73 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.communal.shared.log.communalSceneLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.statusbar.policy.keyguardStateController val Kosmos.communalSceneInteractor: CommunalSceneInteractor by Kosmos.Fixture { @@ -29,5 +30,6 @@ val Kosmos.communalSceneInteractor: CommunalSceneInteractor by repository = communalSceneRepository, logger = communalSceneLogger, sceneInteractor = sceneInteractor, + keyguardStateController = keyguardStateController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt new file mode 100644 index 000000000000..3452d097d3da --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/util/UserTouchActivityNotifierKosmos.kt @@ -0,0 +1,28 @@ +/* + * 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.communal.util + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.power.domain.interactor.powerInteractor + +val Kosmos.userTouchActivityNotifier by + Kosmos.Fixture { + UserTouchActivityNotifier(backgroundScope, powerInteractor, USER_TOUCH_ACTIVITY_RATE_LIMIT) + } + +const val USER_TOUCH_ACTIVITY_RATE_LIMIT = 100 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt index 2a46437ed33e..2a3bd335bf98 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/ui/viewmodel/UdfpsAccessibilityOverlayViewModelKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.deviceentry.data.ui.viewmodel import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor +import com.android.systemui.biometrics.udfpsUtils +import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.keyguard.ui.viewmodel.deviceEntryForegroundIconViewModel import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel @@ -30,5 +32,15 @@ val Kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel by accessibilityInteractor = accessibilityInteractor, deviceEntryIconViewModel = deviceEntryIconViewModel, deviceEntryFgIconViewModel = deviceEntryForegroundIconViewModel, + udfpsUtils = udfpsUtils, + ) + } + +val Kosmos.alternateBouncerUdfpsAccessibilityOverlayViewModel by + Kosmos.Fixture { + AlternateBouncerUdfpsAccessibilityOverlayViewModel( + udfpsOverlayInteractor = udfpsOverlayInteractor, + accessibilityInteractor = accessibilityInteractor, + udfpsUtils = udfpsUtils, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt index bc35dc8052ec..be5431c3d0d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AccessibilityActionsViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -27,5 +28,6 @@ val Kosmos.accessibilityActionsViewModelKosmos by Fixture { communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, keyguardInteractor = keyguardInteractor, + udfpsOverlayInteractor = udfpsOverlayInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt new file mode 100644 index 000000000000..625e751c8b65 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToPrimaryBouncerViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.dreamingToPrimaryBouncerViewModel by Fixture { + DreamingToPrimaryBouncerTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + blurConfig = blurConfig, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt index 530981c489e8..02e63a42d87d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt @@ -17,15 +17,21 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.ui.glanceableHubBlurComponentFactory import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture { GlanceableHubToLockscreenTransitionViewModel( + applicationScope = applicationCoroutineScope, configurationInteractor = configurationInteractor, animationFlow = keyguardTransitionAnimationFlow, + communalSceneInteractor = communalSceneInteractor, + communalSettingsInteractor = communalSettingsInteractor, blurFactory = glanceableHubBlurComponentFactory, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..c53bf9556aca --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDreamingTransitionViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.blurConfig +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.primaryBouncerToDreamingTransitionViewModel by Fixture { + PrimaryBouncerToDreamingTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + blurConfig = blurConfig, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt index 1397d974cbc5..20d3682c6964 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt @@ -21,6 +21,7 @@ import android.window.WindowContext import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker import com.android.systemui.shade.ShadeWindowLayoutParams import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository @@ -60,5 +61,6 @@ val Kosmos.shadeDisplaysInteractor by notificationRebindingTracker, notificationStackRebindingHider, configurationController, + logcatLogBuffer("ShadeDisplaysInteractor"), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 23251d27cff9..90e23290e9e9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -22,9 +22,7 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarou import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory val Kosmos.notificationsShadeOverlayContentViewModel: @@ -34,9 +32,7 @@ val Kosmos.notificationsShadeOverlayContentViewModel: notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory, sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, - shadeModeInteractor = shadeModeInteractor, disableFlagsInteractor = disableFlagsInteractor, mediaCarouselInteractor = mediaCarouselInteractor, - activeNotificationsInteractor = activeNotificationsInteractor, ) } 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 c4542c4e709b..00b26c944b90 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 @@ -19,7 +19,7 @@ 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.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC 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 @@ -40,7 +40,7 @@ fun Kosmos.setPromotedContent(entry: NotificationEntry) { promotedNotificationContentExtractor.extractContent( entry, Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification), - REDACTION_TYPE_NONE, + REDACTION_TYPE_PUBLIC, RowImageInflater.newInstance(previousIndex = null, reinflating = false) .useForContentModel(), ) 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 fcd484353011..ea459a95728a 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 @@ -17,12 +17,16 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor val Kosmos.aodPromotedNotificationInteractor by Kosmos.Fixture { AODPromotedNotificationInteractor( promotedNotificationsInteractor = promotedNotificationsInteractor, + keyguardInteractor = keyguardInteractor, + sensitiveNotificationProtectionInteractor = sensitiveNotificationProtectionInteractor, dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 8fa82cad5c32..72f9d550eb4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,7 +26,6 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class FakeMobileIconsInteractor( mobileMappings: MobileMappingsProxy, @@ -76,7 +75,7 @@ class FakeMobileIconsInteractor( override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList()) - override val isStackable: StateFlow<Boolean> = MutableStateFlow(false) + override val isStackable: MutableStateFlow<Boolean> = MutableStateFlow(false) private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt new file mode 100644 index 000000000000..ba4410b51b75 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/SensitiveNotificationProtectionInteractorKosmos.kt @@ -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. + */ + +package com.android.systemui.statusbar.policy.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.sensitiveNotificationProtectionController + +var Kosmos.sensitiveNotificationProtectionInteractor: SensitiveNotificationProtectionInteractor by + Kosmos.Fixture { + SensitiveNotificationProtectionInteractor(sensitiveNotificationProtectionController) + } diff --git a/ravenwood/README.md b/ravenwood/README.md index 9c4fda7a50a6..62f2ffae56ba 100644 --- a/ravenwood/README.md +++ b/ravenwood/README.md @@ -4,25 +4,4 @@ Ravenwood is an officially-supported lightweight unit testing environment for An Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing. -## Background - -Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness. - -In contrast, defining a lightweight unit testing environment mitigates these issues by running directly from build artifacts (no flashing required), runs immediately (no booting required), and runs in an isolated environment (less flakiness). - -## Guiding principles -Here’s a summary of the guiding principles for Ravenwood, aimed at addressing Robolectric design concerns and better supporting Android platform developers: - -* **API support for Ravenwood is opt-in.** Teams that own APIs decide exactly what, and how, they support their API functionality being available to tests. When an API hasn’t opted-in, the API signatures remain available for tests to compile against and/or mock, but they throw when called under a Ravenwood environment. - * _Contrasted with Robolectric which attempts to run API implementations as-is, causing maintenance pains as teams maintain or redesign their API internals._ -* **API support and customizations for Ravenwood appear directly inline with relevant code.** This improves maintenance of APIs by providing awareness of what code runs under Ravenwood, including the ability to replace code at a per-method level when Ravenwood-specific customization is needed. - * _Contrasted with Robolectric which maintains customized behavior in separate “Shadow” classes that are difficult for maintainers to be aware of._ -* **APIs supported under Ravenwood are tested to remain consistent with physical devices.** As teams progressively opt-in supporting APIs under Ravenwood, we’re requiring they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device. - * _Contrasted with Robolectric, which has limited (and forked) testing of their environment, increasing their risk of accidental divergence over time and misleading “passing” signals._ -* **Ravenwood aims to support more “real” code.** As API owners progressively opt-in their code, they have the freedom to provide either a limited “fake” that is a faithful emulation of how a device behaves, or they can bring more “real” code that runs on physical devices. - * _Contrasted with Robolectric, where support for “real” code ends at the app process boundary, such as a call into `system_server`._ - -## More details - -* [Ravenwood for Test Authors](test-authors.md) -* [Ravenwood for API Maintainers](api-maintainers.md) +Documents have been moved to go/ravenwood. diff --git a/ravenwood/api-maintainers.md b/ravenwood/api-maintainers.md deleted file mode 100644 index 142492eace98..000000000000 --- a/ravenwood/api-maintainers.md +++ /dev/null @@ -1,122 +0,0 @@ -# Ravenwood for API Maintainers - -By default, Android APIs aren’t opted-in to Ravenwood, and they default to throwing when called under the Ravenwood environment. - -To opt-in to supporting an API under Ravenwood, you can use the inline annotations documented below to customize your API behavior when running under Ravenwood. Because these annotations are inline in the relevant platform source code, they serve as valuable reminders to future API maintainers of Ravenwood support expectations. - -> **Note:** to ensure that API teams are well-supported during early Ravenwood onboarding, the Ravenwood team is manually maintaining an allow-list of classes that are able to use Ravenwood annotations. Please reach out to ravenwood@ so we can offer design advice and allow-list your APIs. - -These Ravenwood-specific annotations have no bearing on the status of an API being public, `@SystemApi`, `@TestApi`, `@hide`, etc. Ravenwood annotations are an orthogonal concept that are only consumed by the internal `hoststubgen` tool during a post-processing step that generates the Ravenwood runtime environment. Teams that own APIs can continue to refactor opted-in `@hide` implementation details, as long as the test-visible behavior continues passing. - -As described in our Guiding Principles, when a team opts-in an API, we’re requiring that they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device. At the moment this means adding the bivalent tests to relevant `TEST_MAPPING` files to ensure they remain consistently passing over time. These bivalent tests are important because they progressively provide the foundation on which higher-level unit tests place their trust. - -## Opt-in to supporting a single method while other methods remained opt-out - -``` -@RavenwoodKeepPartialClass -public class MyManager { - @RavenwoodKeep - public static String modeToString(int mode) { - // This method implementation runs as-is on both devices and Ravenwood - } - - public static void doComplex() { - // This method implementation runs as-is on devices, but because there - // is no method-level annotation, and the class-level default is - // “keep partial”, this method is not supported under Ravenwood and - // will throw - } -} -``` - -## Opt-in an entire class with opt-out of specific methods - -``` -@RavenwoodKeepWholeClass -public class MyStruct { - public void doSimple() { - // This method implementation runs as-is on both devices and Ravenwood, - // implicitly inheriting the class-level annotation - } - - @RavenwoodThrow - public void doComplex() { - // This method implementation runs as-is on devices, but the - // method-level annotation overrides the class-level annotation, so - // this method is not supported under Ravenwood and will throw - } -} -``` - -## Replace a complex method when under Ravenwood - -``` -@RavenwoodKeepWholeClass -public class MyStruct { - @RavenwoodReplace - public void doComplex() { - // This method implementation runs as-is on devices, but the - // implementation is replaced/substituted by the - // doComplex$ravenwood() method implementation under Ravenwood - } - - private void doComplex$ravenwood() { - // This method implementation only runs under Ravenwood. - // The visibility of this replacement method does not need to match - // the original method, so it's recommmended to always use - // private methods so that these methods won't be accidentally used - // by unexpected users. - } -} -``` - -## Redirect a complex method to a separate class when under Ravenwood - -``` -@RavenwoodKeepPartialClass -@RavenwoodRedirectionClass("MyComplexClass_ravenwood") -public class MyComplexClass { - @RavenwoodRedirect - public void doComplex(int i, int j, int k) { - // This method implementation runs as-is on devices, but calls to this - // method is redirected to MyComplexClass_ravenwood#doComplex() - // under Ravenwood. - } - - @RavenwoodRedirect - public static void staticDoComplex(int i, int j, int k) { - // This method implementation runs as-is on devices, but calls to this - // method is redirected to MyComplexClass_ravenwood#staticDoComplex() - // under Ravenwood. - } - -/** - * This class is only available in the Ravenwood environment. - */ -class MyComplexClass_ravenwood { - public static void doComplex(MyComplexClass obj, int i, int j, int k) { - // Because the original method is a non-static method, the current - // object instance of the original method (the "this" reference) - // will be passed over as the first argument of the redirection method, - // with the remaining arguments to follow. - } - - public static void staticDoComplex(int i, int j, int k) { - // Because the original method is a static method, there is no current - // object instance, so the parameter list of the redirection method - // is the same as the original method. - } -} -``` - -## General strategies for side-stepping tricky dependencies - -The “replace” strategy described above is quite powerful, and can be used in creative ways to sidestep tricky underlying dependencies that aren’t ready yet. - -For example, consider a constructor or static initializer that relies on unsupported functionality from another team. By factoring the unsupported logic into a dedicated method, that method can then be replaced under Ravenwood to offer baseline functionality. - -## Strategies for JNI - -At the moment, JNI support is considered "experimental". To ensure that teams are well-supported, for any JNI usage, please reach out to ravenwood@ so we can offer design advice and help you onboard native methods. If a native method can be trivially re-implemented in pure-Java, using the replacement or redirection mechanisms described above is also a viable option. - -The main caveat regarding JNI support on Ravenwood is due to how native code is built for Ravenwood. Unlike Java/Kotlin code where Ravenwood directly uses the same intermediate artifacts when building the real target for Android, Ravenwood uses the "host" variant of native libraries. The "host" variant of native libraries usually either don't exist, are extremely limited, or behave drastically different compared to the Android variant. diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md deleted file mode 100644 index b1030ff4e7ed..000000000000 --- a/ravenwood/test-authors.md +++ /dev/null @@ -1,175 +0,0 @@ -# Ravenwood for Test Authors - -The Ravenwood testing environment runs inside a single Java process on the host side, and provides a limited yet growing set of Android API functionality. - -Ravenwood explicitly does not support “large” integration tests that expect a fully booted Android OS. Instead, it’s more suited for “small” and “medium” tests where your code-under-test has been factored to remove dependencies on a fully booted device. - -When writing tests under Ravenwood, all Android API symbols associated with your declared `sdk_version` are available to link against using, but unsupported APIs will throw an exception. This design choice enables mocking of unsupported APIs, and supports sharing of test code to build “bivalent” test suites that run against either Ravenwood or a traditional device. - -## Manually running tests - -To run all Ravenwood tests, use: - -``` -./frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh -``` - -To run a specific test, use "atest" as normal, selecting the test from a Ravenwood suite such as: - -``` -atest CtsOsTestCasesRavenwood:ParcelTest#testSetDataCapacityNegative -``` - -## Typical test structure - -Below are the typical steps needed to add a straightforward “small” unit test: - -* Define an `android_ravenwood_test` rule in your `Android.bp` file: - -``` -android_ravenwood_test { - name: "MyTestsRavenwood", - static_libs: [ - "androidx.annotation_annotation", - "androidx.test.ext.junit", - "androidx.test.rules", - ], - srcs: [ - "src/com/example/MyCode.java", - "tests/src/com/example/MyCodeTest.java", - ], - sdk_version: "test_current", - auto_gen_config: true, -} -``` - -* Write your unit test just like you would for an Android device: - -``` -import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class MyCodeTest { - @Test - public void testSimple() { - // ... - } -} -``` - -Once you’ve defined your test, you can use typical commands to execute it locally: - -``` -$ atest MyTestsRavenwood -``` - -You can also run your new tests automatically via `TEST_MAPPING` rules like this: - -``` -{ - "ravenwood-presubmit": [ - { - "name": "MyTestsRavenwood", - "host": true - } - ] -} -``` - -## Using resources - -At the moment, the `android_ravenwood_test` module type cannot directly build resources yet. In order to use resources in Ravenwood tests, you have to build the resource APK in a separate `android_app` module and "borrow" the resources and R classes: - -``` -android_app { - name: "MyTests-res", - resource_dirs: ["res"], - // This has to be set to false, or ".aapt.srcjar" will not be generated - use_resource_processor: false, -} - -android_ravenwood_test { - name: "MyTestsRavenwood", - srcs: [ - "src/**/*.java", - // ... - - // Include R.java from the resource APK - ":MyTests-res{.aapt.srcjar}", - ], - // Set the resource APK - resource_apk: "MyTests-res", - // ... -} -``` - -## Strategies for migration/bivalent tests - -Ravenwood aims to support tests that are written in a “bivalent” way, where the same test code can be dual-compiled to run on both a real Android device and under a Ravenwood environment. - -In situations where a test method depends on API functionality not yet available under Ravenwood, we provide an annotation to quietly skip that test under Ravenwood, while continuing to validate that test on real devices. The annotation can be applied to either individual methods or to an entire test class. - -Test authors are encouraged to provide a `blockedBy` or `reason` argument to help future maintainers understand why a test is being ignored, and under what conditions it might be supported in the future. - -``` -@RunWith(AndroidJUnit4.class) -public class MyCodeTest { - @Test - public void testSimple() { - // Simple test that runs on both devices and Ravenwood - } - - @Test - @DisabledOnRavenwood(blockedBy = PackageManager.class) - public void testComplex() { - // Complex test that runs on devices, but is disabled under Ravenwood - } -} -``` - -## Strategies for unsupported APIs - -As you write tests against Ravenwood, you’ll likely discover API dependencies that aren’t supported yet. Here’s a few strategies that can help you make progress: - -* Your code-under-test may benefit from subtle dependency refactoring to reduce coupling. (For example, providing a specific `File` argument instead of deriving paths internally from a `Context` or `Environment`.) - * One common use-case is providing a directory for your test to store temporary files, which can easily be accomplished using the `Files.createTempDirectory()` API which works on both physical devices and under Ravenwood: - -``` -import java.nio.file.Files; - -@RunWith(AndroidJUnit4.class) -public class MyTest { - @Before - public void setUp() throws Exception { - File tempDir = Files.createTempDirectory("MyTest").toFile(); -... -``` - -* Although mocking code that your team doesn’t own is a generally discouraged testing practice, it can be a valuable pressure relief valve when a dependency isn’t yet supported. - -## Strategies for debugging test development - -When writing tests you may encounter odd or hard to debug behaviors. One good place to start is at the beginning of the logs stored by atest: - -``` -$ atest MyTestsRavenwood -... -Test Logs have saved in /tmp/atest_result/20231128_094010_0e90t8v8/log -Run 'atest --history' to review test result history. -``` - -The most useful logs are in the `isolated-java-logs` text file, which can typically be tab-completed by copy-pasting the logs path mentioned in the atest output: - -``` -$ less /tmp/atest_result/20231128_133105_h9al__79/log/i*/i*/isolated-java-logs* -``` - -Here are some common known issues and recommended workarounds: - -* Some code may unconditionally interact with unsupported APIs, such as via static initializers. One strategy is to shift the logic into `@Before` methods and make it conditional by testing `RavenwoodRule.isOnRavenwood()`. diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 60343e9e81e5..99febd6de60f 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -81,10 +81,16 @@ import com.android.server.accessibility.Flags; public class AutoclickController extends BaseEventStreamTransformation { private static final String LOG_TAG = AutoclickController.class.getSimpleName(); + // TODO(b/393559560): Finalize scroll amount. + private static final float SCROLL_AMOUNT = 1.0f; private final AccessibilityTraceManager mTrace; private final Context mContext; private final int mUserId; + @VisibleForTesting + float mLastCursorX; + @VisibleForTesting + float mLastCursorY; // Lazily created on the first mouse motion event. @VisibleForTesting ClickScheduler mClickScheduler; @@ -315,8 +321,58 @@ public class AutoclickController extends BaseEventStreamTransformation { /** * Handles scroll operations in the specified direction. */ - public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { - // TODO(b/388845721): Perform actual scroll. + private void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) { + final long now = SystemClock.uptimeMillis(); + + // Create pointer properties. + PointerProperties[] pointerProps = new PointerProperties[1]; + pointerProps[0] = new PointerProperties(); + pointerProps[0].id = 0; + pointerProps[0].toolType = MotionEvent.TOOL_TYPE_MOUSE; + + // Create pointer coordinates at the last cursor position. + PointerCoords[] pointerCoords = new PointerCoords[1]; + pointerCoords[0] = new PointerCoords(); + pointerCoords[0].x = mLastCursorX; + pointerCoords[0].y = mLastCursorY; + + // Set scroll values based on direction. + switch (direction) { + case AutoclickScrollPanel.DIRECTION_UP: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_DOWN: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, -SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_LEFT: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_RIGHT: + pointerCoords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, -SCROLL_AMOUNT); + break; + case AutoclickScrollPanel.DIRECTION_EXIT: + case AutoclickScrollPanel.DIRECTION_NONE: + default: + return; + } + + // Get device ID from last motion event if possible. + int deviceId = mClickScheduler != null && mClickScheduler.mLastMotionEvent != null + ? mClickScheduler.mLastMotionEvent.getDeviceId() : 0; + + // Create a scroll event. + MotionEvent scrollEvent = MotionEvent.obtain( + /* downTime= */ now, /* eventTime= */ now, + MotionEvent.ACTION_SCROLL, /* pointerCount= */ 1, pointerProps, + pointerCoords, /* metaState= */ 0, /* actionButton= */ 0, /* xPrecision= */ + 1.0f, /* yPrecision= */ 1.0f, deviceId, /* edgeFlags= */ 0, + InputDevice.SOURCE_MOUSE, /* flags= */ 0); + + // Send the scroll event. + super.onMotionEvent(scrollEvent, scrollEvent, mClickScheduler.mEventPolicyFlags); + + // Clean up. + scrollEvent.recycle(); } /** @@ -823,13 +879,19 @@ public class AutoclickController extends BaseEventStreamTransformation { // If exit button is hovered, exit scroll mode after countdown and return early. if (mHoveredDirection == AutoclickScrollPanel.DIRECTION_EXIT) { exitScrollMode(); + return; } - return; } // Handle scroll type specially, show scroll panel instead of sending click events. if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) { if (mAutoclickScrollPanel != null) { + // Save the last cursor position at the moment when sendClick() is called. + if (mClickScheduler != null && mClickScheduler.mLastMotionEvent != null) { + final int pointerIndex = mClickScheduler.mLastMotionEvent.getActionIndex(); + mLastCursorX = mClickScheduler.mLastMotionEvent.getX(pointerIndex); + mLastCursorY = mClickScheduler.mLastMotionEvent.getY(pointerIndex); + } mAutoclickScrollPanel.show(); } return; diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java index c71443149687..025423078da1 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java @@ -179,7 +179,7 @@ public class AutoclickScrollPanel { private WindowManager.LayoutParams getLayoutParams() { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars()); layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; diff --git a/services/art-profile-extra b/services/art-profile-extra index 54362411e5ea..9cbc03903904 100644 --- a/services/art-profile-extra +++ b/services/art-profile-extra @@ -1 +1,9 @@ HSPLcom/android/server/am/ActivityManagerService$LocalService;->checkContentProviderAccess(Ljava/lang/String;I)Ljava/lang/String; +HSPLcom/android/server/am/ActivityManagerService$LocalService;->updateDeviceIdleTempAllowlist([IIZJIILjava/lang/String;I)V +HSPLcom/android/server/am/OomAdjuster;->setUidTempAllowlistStateLSP(IZ)V +HSPLcom/android/server/am/BatteryStatsService;->setBatteryState(IIIIIIIIJ)V +HSPLcom/android/server/pm/PackageManagerService$IPackageManagerImpl;->setComponentEnabledSetting(Landroid/content/ComponentName;IIILjava/lang/String;)V +HSPLcom/android/server/am/ActiveServices;->bindServiceLocked(Landroid/app/IApplicationThread;Landroid/os/IBinder;Landroid/content/Intent;Ljava/lang/String;Landroid/app/IServiceConnection;JLjava/lang/String;ZILjava/lang/String;Landroid/app/IApplicationThread;Ljava/lang/String;I)I +HSPLcom/android/server/accessibility/AccessibilityManagerService;->onServiceInfoChangedLocked(Lcom/android/server/accessibility/AccessibilityUserState;)V +HSPLcom/android/server/clipboard/ClipboardService$ClipboardImpl;->checkAndSetPrimaryClip(Landroid/content/ClipData;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V +HSPLcom/android/server/clipboard/ClipboardService$ClipboardImpl;->getPrimaryClip(Ljava/lang/String;Ljava/lang/String;II)Landroid/content/ClipData; diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 1d914c89c570..6ac2180176ce 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -85,6 +85,8 @@ import com.android.internal.os.IBinaryTransparencyService; import com.android.internal.util.FrameworkStatsLog; import com.android.modules.expresslog.Histogram; import com.android.server.pm.ApexManager; +import com.android.server.pm.BackgroundInstallControlCallbackHelper; +import com.android.server.pm.BackgroundInstallControlService; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.AndroidPackageSplit; import com.android.server.pm.pkg.PackageState; @@ -101,9 +103,6 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.stream.Collectors; -import com.android.server.pm.BackgroundInstallControlService; -import com.android.server.pm.BackgroundInstallControlCallbackHelper; - /** * @hide */ @@ -1577,19 +1576,17 @@ public class BinaryTransparencyService extends SystemService { Slog.d(TAG, String.format("VBMeta Digest: %s", mVbmetaDigest)); FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest); - if (android.security.Flags.binaryTransparencySepolicyHash()) { - IoThread.getExecutor().execute(() -> { - byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( - "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); - String sepolicyHashEncoded = null; - if (sepolicyHash != null) { - sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); - Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); - } - FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, - sepolicyHashEncoded, mVbmetaDigest); - }); - } + IoThread.getExecutor().execute(() -> { + byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( + "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); + String sepolicyHashEncoded = null; + if (sepolicyHash != null) { + sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); + Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); + } + FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, + sepolicyHashEncoded, mVbmetaDigest); + }); } /** diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index 87222a60d82d..28258ae47a65 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -19,7 +19,6 @@ package com.android.server; import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; import static android.service.quickaccesswallet.Flags.launchWalletViaSysuiCallbacks; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; import android.app.ActivityManager; @@ -635,46 +634,6 @@ public class GestureLauncherService extends SystemService { } /** - * Processes a power key event in GestureLauncherService without performing an action. This - * method is called on every KEYCODE_POWER ACTION_DOWN event and ensures that, even if - * KEYCODE_POWER events are passed to and handled by the app, the GestureLauncherService still - * keeps track of all running KEYCODE_POWER events for its gesture detection and relevant - * actions. - */ - public void processPowerKeyDown(KeyEvent event) { - if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 - && event.getEventTime() - mLastEmergencyGestureTriggered - < mEmergencyGesturePowerButtonCooldownPeriodMs) { - return; - } - if (event.isLongPress()) { - return; - } - - final long powerTapInterval; - - synchronized (this) { - powerTapInterval = event.getEventTime() - mLastPowerDown; - mLastPowerDown = event.getEventTime(); - if (powerTapInterval >= POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS) { - // Tap too slow, reset consecutive tap counts. - mFirstPowerDown = event.getEventTime(); - mPowerButtonConsecutiveTaps = 1; - mPowerButtonSlowConsecutiveTaps = 1; - } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { - // Tap too slow for shortcuts - mFirstPowerDown = event.getEventTime(); - mPowerButtonConsecutiveTaps = 1; - mPowerButtonSlowConsecutiveTaps++; - } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) { - // Fast consecutive tap - mPowerButtonConsecutiveTaps++; - mPowerButtonSlowConsecutiveTaps++; - } - } - } - - /** * Attempts to intercept power key down event by detecting certain gesture patterns * * @param interactive true if the event's policy contains {@code FLAG_INTERACTIVE} @@ -721,7 +680,7 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps++; - } else if (powerTapInterval > 0) { + } else { // Fast consecutive tap mPowerButtonConsecutiveTaps++; mPowerButtonSlowConsecutiveTaps++; diff --git a/services/core/java/com/android/server/connectivity/PacProxyService.java b/services/core/java/com/android/server/connectivity/PacProxyService.java index 2e90a3d86161..c8c1eddd53e7 100644 --- a/services/core/java/com/android/server/connectivity/PacProxyService.java +++ b/services/core/java/com/android/server/connectivity/PacProxyService.java @@ -36,6 +36,7 @@ import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.IServiceManager; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ServiceManager; @@ -355,7 +356,9 @@ public class PacProxyService extends IPacProxyManager.Stub { } catch (RemoteException e1) { Log.e(TAG, "Remote Exception", e1); } - ServiceManager.addService(PAC_SERVICE_NAME, binder); + // Do not cache the service, otherwise it will crash com.android.pacprocessor + ServiceManager.addService(PAC_SERVICE_NAME, binder, /* allowIsolated */ false, + IServiceManager.FLAG_IS_LAZY_SERVICE); mProxyService = IProxyService.Stub.asInterface(binder); if (mProxyService == null) { Log.e(TAG, "No proxy service"); diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index f2ededaa2a9c..07530e1c6f7b 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -344,6 +344,9 @@ public class InputManagerService extends IInputManager.Stub // Manages battery state for input devices. private final BatteryController mBatteryController; + // Monitors any changes to the sysfs nodes when an input device is connected. + private final SysfsNodeMonitor mSysfsNodeMonitor; + @Nullable private final TouchpadDebugViewController mTouchpadDebugViewController; @@ -536,6 +539,8 @@ public class InputManagerService extends IInputManager.Stub injector.getLooper(), this) : null; mBatteryController = new BatteryController(mContext, mNative, injector.getLooper(), injector.getUEventManager()); + mSysfsNodeMonitor = new SysfsNodeMonitor(mContext, mNative, injector.getLooper(), + injector.getUEventManager()); mKeyboardBacklightController = injector.getKeyboardBacklightController(mNative); mStickyModifierStateController = new StickyModifierStateController(); mInputDataStore = new InputDataStore(); @@ -665,6 +670,7 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager.systemRunning(); mBatteryController.systemRunning(); + mSysfsNodeMonitor.systemRunning(); mKeyboardBacklightController.systemRunning(); mKeyboardLedController.systemRunning(); mKeyRemapper.systemRunning(); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index ccf1a2c90876..de54cd81aa43 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -272,6 +272,9 @@ interface NativeInputManagerService { /** Set whether showing a pointer icon for styluses is enabled. */ void setStylusPointerIconEnabled(boolean enabled); + /** Get the sysfs root path of an input device if known, otherwise return null. */ + @Nullable String getSysfsRootPath(int deviceId); + /** * Report sysfs node changes. This may result in recreation of the corresponding InputDevice. * The recreated device may contain new associated peripheral devices like Light, Battery, etc. @@ -619,6 +622,9 @@ interface NativeInputManagerService { public native void setStylusPointerIconEnabled(boolean enabled); @Override + public native String getSysfsRootPath(int deviceId); + + @Override public native void sysfsNodeChanged(String sysfsNodePath); @Override diff --git a/services/core/java/com/android/server/input/SysfsNodeMonitor.java b/services/core/java/com/android/server/input/SysfsNodeMonitor.java new file mode 100644 index 000000000000..e55e1284d03c --- /dev/null +++ b/services/core/java/com/android/server/input/SysfsNodeMonitor.java @@ -0,0 +1,203 @@ +/* + * Copyright 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.server.input; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Looper; +import android.os.UEventObserver; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.Objects; + +/** + * A thread-safe component of {@link InputManagerService} responsible for monitoring the addition + * of kernel sysfs nodes for newly connected input devices. + * + * This class uses the {@link UEventObserver} to monitor for changes to an input device's sysfs + * nodes, and is responsible for requesting the native code to refresh its sysfs nodes when there + * is a change. This is necessary because the sysfs nodes may only be configured after an input + * device is already added, with no way for the native code to detect any changes afterwards. + */ +final class SysfsNodeMonitor { + private static final String TAG = SysfsNodeMonitor.class.getSimpleName(); + + // To enable these logs, run: + // 'adb shell setprop log.tag.SysfsNodeMonitor DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final long SYSFS_NODE_MONITORING_TIMEOUT_MS = 60_000; // 1 minute + + private final Context mContext; + private final NativeInputManagerService mNative; + private final Handler mHandler; + private final UEventManager mUEventManager; + + private InputManager mInputManager; + + private final SparseArray<SysfsNodeAddedListener> mUEventListenersByDeviceId = + new SparseArray<>(); + + SysfsNodeMonitor(Context context, NativeInputManagerService nativeService, Looper looper, + UEventManager uEventManager) { + mContext = context; + mNative = nativeService; + mHandler = new Handler(looper); + mUEventManager = uEventManager; + } + + public void systemRunning() { + mInputManager = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); + mInputManager.registerInputDeviceListener(mInputDeviceListener, mHandler); + for (int deviceId : mInputManager.getInputDeviceIds()) { + mInputDeviceListener.onInputDeviceAdded(deviceId); + } + } + + private final InputManager.InputDeviceListener mInputDeviceListener = + new InputManager.InputDeviceListener() { + @Override + public void onInputDeviceAdded(int deviceId) { + startMonitoring(deviceId); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + stopMonitoring(deviceId); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + } + }; + + private void startMonitoring(int deviceId) { + final var inputDevice = mInputManager.getInputDevice(deviceId); + if (inputDevice == null) { + return; + } + if (!inputDevice.isExternal()) { + if (DEBUG) { + Log.d(TAG, "Not listening to sysfs node changes for internal input device: " + + deviceId); + } + return; + } + final var sysfsRootPath = formatDevPath(mNative.getSysfsRootPath(deviceId)); + if (sysfsRootPath == null) { + if (DEBUG) { + Log.d(TAG, "Sysfs node not found for external input device: " + deviceId); + } + return; + } + if (DEBUG) { + Log.d(TAG, "Start listening to sysfs node changes for input device: " + deviceId + + ", node: " + sysfsRootPath); + } + final var listener = new SysfsNodeAddedListener(); + mUEventListenersByDeviceId.put(deviceId, listener); + + // We must synchronously start monitoring for changes to this device's path. + // Once monitoring starts, we need to trigger a native refresh of the sysfs nodes to + // catch any changes that happened between the input device's creation and the UEvent + // listener being added. + // NOTE: This relies on the fact that the following `addListener` call is fully synchronous. + mUEventManager.addListener(listener, sysfsRootPath); + mNative.sysfsNodeChanged(sysfsRootPath); + + // Always stop listening for new sysfs nodes after the timeout. + mHandler.postDelayed(() -> stopMonitoring(deviceId), SYSFS_NODE_MONITORING_TIMEOUT_MS); + } + + private static String formatDevPath(String path) { + // Remove the "/sys" prefix if it has one. + return path != null && path.startsWith("/sys") ? path.substring(4) : path; + } + + private void stopMonitoring(int deviceId) { + final var listener = mUEventListenersByDeviceId.removeReturnOld(deviceId); + if (listener == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "Stop listening to sysfs node changes for input device: " + deviceId); + } + mUEventManager.removeListener(listener); + } + + class SysfsNodeAddedListener extends UEventManager.UEventListener { + + private boolean mHasReceivedRemovalNotification = false; + private boolean mHasReceivedPowerSupplyNotification = false; + + @Override + public void onUEvent(UEventObserver.UEvent event) { + // This callback happens on the UEventObserver's thread. + // Ensure we are processing on the handler thread. + mHandler.post(() -> handleUEvent(event)); + } + + private void handleUEvent(UEventObserver.UEvent event) { + if (DEBUG) { + Slog.d(TAG, "UEventListener: Received UEvent: " + event); + } + final var subsystem = event.get("SUBSYSTEM"); + final var devPath = "/sys" + Objects.requireNonNull( + TextUtils.nullIfEmpty(event.get("DEVPATH"))); + final var action = event.get("ACTION"); + + // NOTE: We must be careful to avoid reconfiguring sysfs nodes during device removal, + // because it might result in the device getting re-opened in native code during + // removal, resulting in unexpected states. If we see any removal action for this node, + // ensure we stop responding altogether. + if (mHasReceivedRemovalNotification || "REMOVE".equalsIgnoreCase(action)) { + mHasReceivedRemovalNotification = true; + return; + } + + if ("LEDS".equalsIgnoreCase(subsystem) && "ADD".equalsIgnoreCase(action)) { + // An LED node was added. Notify native code to reconfigure the sysfs node. + if (DEBUG) { + Slog.d(TAG, + "Reconfiguring sysfs node because 'leds' node was added: " + devPath); + } + mNative.sysfsNodeChanged(devPath); + return; + } + + if ("POWER_SUPPLY".equalsIgnoreCase(subsystem)) { + if (mHasReceivedPowerSupplyNotification) { + return; + } + // This is the first notification we received from the power_supply subsystem. + // Notify native code that the battery node may have been added. The power_supply + // subsystem does not seem to be sending ADD events, so use use the first event + // with any action as a proxy for a new power_supply node being created. + if (DEBUG) { + Slog.d(TAG, "Reconfiguring sysfs node because 'power_supply' node had action '" + + action + "': " + devPath); + } + mHasReceivedPowerSupplyNotification = true; + mNative.sysfsNodeChanged(devPath); + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java index 9d889839879b..c2873e8ee28e 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java @@ -256,9 +256,11 @@ final class IInputMethodClientInvoker { @AnyThread private void setImeVisibilityInternal(boolean visible, @Nullable ImeTracker.Token statsToken) { try { + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_INVOKER); mTarget.setImeVisibility(visible, statsToken); } catch (RemoteException e) { logRemoteException(e); + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_INVOKER); } } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index 0047ec20d691..3fcb6ce271e3 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -205,9 +205,11 @@ final class IInputMethodInvoker { boolean showSoftInput(IBinder showInputToken, @NonNull ImeTracker.Token statsToken, @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) { try { + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver); } catch (RemoteException e) { logRemoteException(e); + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); return false; } return true; @@ -218,9 +220,11 @@ final class IInputMethodInvoker { boolean hideSoftInput(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver) { try { + ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver); } catch (RemoteException e) { logRemoteException(e); + ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_IME_INVOKER); return false; } return true; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 7ff41e309c55..87259d80554f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3206,7 +3206,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // TODO(b/353463205) check callers to see if we can make statsToken @NonNull - boolean showCurrentInputInternal(IBinder windowToken, @Nullable ImeTracker.Token statsToken) { + boolean showCurrentInputInternal(IBinder windowToken, @NonNull ImeTracker.Token statsToken) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#showSoftInput", mDumper); @@ -3226,7 +3226,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // TODO(b/353463205) check callers to see if we can make statsToken @NonNull - boolean hideCurrentInputInternal(IBinder windowToken, @Nullable ImeTracker.Token statsToken) { + boolean hideCurrentInputInternal(IBinder windowToken, @NonNull ImeTracker.Token statsToken) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#hideSoftInput", mDumper); diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java index 65b0ad0d61a0..1e8ebca7f336 100644 --- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java +++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java @@ -39,6 +39,7 @@ import android.os.UserHandle; import android.util.Slog; import com.android.internal.R; +import com.android.media.flags.Flags; import java.util.Collections; import java.util.List; @@ -123,7 +124,9 @@ import java.util.Objects; @Override public synchronized List<MediaRoute2Info> getAvailableRoutes() { - return Collections.emptyList(); + return Flags.enableFixForEmptySystemRoutesCrash() + ? List.of(mDeviceRoute) + : Collections.emptyList(); } @Override diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index c174451e8f5b..5d571de2ce54 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -3269,13 +3269,21 @@ public class MediaSessionService extends SystemService implements Monitor { if (!postedNotification.isMediaNotification()) { return; } + if ((postedNotification.flags & Notification.FLAG_FOREGROUND_SERVICE) == 0) { + // Ignore notifications posted without a foreground service. + return; + } synchronized (mLock) { Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid); if (notifications == null) { notifications = new HashMap<>(); mMediaNotifications.put(uid, notifications); } - notifications.put(sbn.getKey(), sbn); + StatusBarNotification previousSbn = notifications.put(sbn.getKey(), sbn); + if (previousSbn != null) { + // Only act on the first notification update. + return; + } MediaSessionRecordImpl userEngagedRecord = getUserEngagedMediaSessionRecordForNotification(uid, postedNotification); if (userEngagedRecord != null) { diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 0202b554b8aa..937be14c01d4 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -230,21 +230,24 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void updatePictureProfile(String id, PictureProfile pp, int userId) { - Long dbId = mPictureProfileTempIdMap.getKey(id); - if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { - mMqManagerNotifier.notifyOnPictureProfileError(id, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - synchronized (mPictureProfileLock) { - ContentValues values = MediaQualityUtils.getContentValues(dbId, - pp.getProfileType(), - pp.getName(), - pp.getPackageName(), - pp.getInputId(), - pp.getParameters()); - updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters()); - } + mHandler.post(() -> { + Long dbId = mPictureProfileTempIdMap.getKey(id); + if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + synchronized (mPictureProfileLock) { + ContentValues values = MediaQualityUtils.getContentValues(dbId, + pp.getProfileType(), + pp.getName(), + pp.getPackageName(), + pp.getInputId(), + pp.getParameters()); + updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, + pp.getParameters()); + } + }); } private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) { @@ -258,35 +261,37 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void removePictureProfile(String id, int userId) { - synchronized (mPictureProfileLock) { - Long dbId = mPictureProfileTempIdMap.getKey(id); + mHandler.post(() -> { + synchronized (mPictureProfileLock) { + Long dbId = mPictureProfileTempIdMap.getKey(id); - PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId); - if (!hasPermissionToRemovePictureProfile(toDelete)) { - mMqManagerNotifier.notifyOnPictureProfileError(id, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - - if (dbId != null) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArgs = {Long.toString(dbId)}; - int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - selection, selectionArgs); - if (result == 0) { + PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId); + if (!hasPermissionToRemovePictureProfile(toDelete)) { mMqManagerNotifier.notifyOnPictureProfileError(id, - PictureProfile.ERROR_INVALID_ARGUMENT, - Binder.getCallingUid(), Binder.getCallingPid()); - } else { - mMqManagerNotifier.notifyOnPictureProfileRemoved( - mPictureProfileTempIdMap.getValue(dbId), toDelete, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); - mPictureProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); + } + + if (dbId != null) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArgs = {Long.toString(dbId)}; + int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + selection, selectionArgs); + if (result == 0) { + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_INVALID_ARGUMENT, + Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnPictureProfileRemoved( + mPictureProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mPictureProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); + } } } - } + }); } private boolean hasPermissionToRemovePictureProfile(PictureProfile toDelete) { @@ -448,56 +453,60 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public SoundProfile createSoundProfile(SoundProfile sp, int userId) { - if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() - && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) - && !hasGlobalSoundQualityServicePermission()) { - mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - - synchronized (mSoundProfileLock) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + public void createSoundProfile(SoundProfile sp, int userId) { + mHandler.post(() -> { + if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() + && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) + && !hasGlobalSoundQualityServicePermission()) { + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - ContentValues values = MediaQualityUtils.getContentValues(null, - sp.getProfileType(), - sp.getName(), - sp.getPackageName() == null || sp.getPackageName().isEmpty() - ? getPackageOfCallingUid() : sp.getPackageName(), - sp.getInputId(), - sp.getParameters()); + synchronized (mSoundProfileLock) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - // id is auto-generated by SQLite upon successful insertion of row - Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - null, values); - MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id); - String value = mSoundProfileTempIdMap.getValue(id); - sp.setProfileId(value); - mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), - Binder.getCallingPid()); - return sp; - } + ContentValues values = MediaQualityUtils.getContentValues(null, + sp.getProfileType(), + sp.getName(), + sp.getPackageName() == null || sp.getPackageName().isEmpty() + ? getPackageOfCallingUid() : sp.getPackageName(), + sp.getInputId(), + sp.getParameters()); + + // id is auto-generated by SQLite upon successful insertion of row + Long id = db.insert(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + null, values); + MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id); + String value = mSoundProfileTempIdMap.getValue(id); + sp.setProfileId(value); + mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), + Binder.getCallingPid()); + } + }); } @GuardedBy("mSoundProfileLock") @Override public void updateSoundProfile(String id, SoundProfile sp, int userId) { - Long dbId = mSoundProfileTempIdMap.getKey(id); - if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { - mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } + mHandler.post(() -> { + Long dbId = mSoundProfileTempIdMap.getKey(id); + if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { + mMqManagerNotifier.notifyOnSoundProfileError(id, + SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - synchronized (mSoundProfileLock) { - ContentValues values = MediaQualityUtils.getContentValues(dbId, - sp.getProfileType(), - sp.getName(), - sp.getPackageName(), - sp.getInputId(), - sp.getParameters()); + synchronized (mSoundProfileLock) { + ContentValues values = MediaQualityUtils.getContentValues(dbId, + sp.getProfileType(), + sp.getName(), + sp.getPackageName(), + sp.getInputId(), + sp.getParameters()); - updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters()); - } + updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters()); + } + }); } private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) { @@ -511,34 +520,36 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public void removeSoundProfile(String id, int userId) { - synchronized (mSoundProfileLock) { - Long dbId = mSoundProfileTempIdMap.getKey(id); - SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); - if (!hasPermissionToRemoveSoundProfile(toDelete)) { - mMqManagerNotifier.notifyOnSoundProfileError(id, - SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - if (dbId != null) { - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArgs = {Long.toString(dbId)}; - int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - selection, - selectionArgs); - if (result == 0) { + mHandler.post(() -> { + synchronized (mSoundProfileLock) { + Long dbId = mSoundProfileTempIdMap.getKey(id); + SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); + if (!hasPermissionToRemoveSoundProfile(toDelete)) { mMqManagerNotifier.notifyOnSoundProfileError(id, - SoundProfile.ERROR_INVALID_ARGUMENT, + SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); - } else { - mMqManagerNotifier.notifyOnSoundProfileRemoved( - mSoundProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mSoundProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); + } + if (dbId != null) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArgs = {Long.toString(dbId)}; + int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + selection, + selectionArgs); + if (result == 0) { + mMqManagerNotifier.notifyOnSoundProfileError(id, + SoundProfile.ERROR_INVALID_ARGUMENT, + Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnSoundProfileRemoved( + mSoundProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mSoundProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); + } } } - } + }); } private boolean hasPermissionToRemoveSoundProfile(SoundProfile toDelete) { @@ -849,14 +860,16 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void setPictureProfileAllowList(List<String> packages, int userId) { - if (!hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit(); - editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); - editor.commit(); + mHandler.post(() -> { + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit(); + editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); + editor.commit(); + }); } @GuardedBy("mSoundProfileLock") @@ -877,13 +890,16 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public void setSoundProfileAllowList(List<String> packages, int userId) { - if (!hasGlobalSoundQualityServicePermission()) { - mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit(); - editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); - editor.commit(); + mHandler.post(() -> { + if (!hasGlobalSoundQualityServicePermission()) { + mMqManagerNotifier.notifyOnSoundProfileError(null, + SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit(); + editor.putString(ALLOWLIST, String.join(COMMA_DELIMITER, packages)); + editor.commit(); + }); } @Override @@ -894,22 +910,24 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void setAutoPictureQualityEnabled(boolean enabled, int userId) { - if (!hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - synchronized (mPictureProfileLock) { - try { - if (mMediaQuality != null) { - if (mMediaQuality.isAutoPqSupported()) { - mMediaQuality.setAutoPqEnabled(enabled); + mHandler.post(() -> { + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + synchronized (mPictureProfileLock) { + try { + if (mMediaQuality != null) { + if (mMediaQuality.isAutoPqSupported()) { + mMediaQuality.setAutoPqEnabled(enabled); + } } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto picture quality", e); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set auto picture quality", e); } - } + }); } @GuardedBy("mPictureProfileLock") @@ -932,22 +950,24 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public void setSuperResolutionEnabled(boolean enabled, int userId) { - if (!hasGlobalPictureQualityServicePermission()) { - mMqManagerNotifier.notifyOnPictureProfileError(null, - PictureProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } - synchronized (mPictureProfileLock) { - try { - if (mMediaQuality != null) { - if (mMediaQuality.isAutoSrSupported()) { - mMediaQuality.setAutoSrEnabled(enabled); + mHandler.post(() -> { + if (!hasGlobalPictureQualityServicePermission()) { + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + synchronized (mPictureProfileLock) { + try { + if (mMediaQuality != null) { + if (mMediaQuality.isAutoSrSupported()) { + mMediaQuality.setAutoSrEnabled(enabled); + } } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set super resolution", e); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set super resolution", e); } - } + }); } @GuardedBy("mPictureProfileLock") @@ -970,22 +990,25 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public void setAutoSoundQualityEnabled(boolean enabled, int userId) { - if (!hasGlobalSoundQualityServicePermission()) { - mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, - Binder.getCallingUid(), Binder.getCallingPid()); - } + mHandler.post(() -> { + if (!hasGlobalSoundQualityServicePermission()) { + mMqManagerNotifier.notifyOnSoundProfileError(null, + SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - synchronized (mSoundProfileLock) { - try { - if (mMediaQuality != null) { - if (mMediaQuality.isAutoAqSupported()) { - mMediaQuality.setAutoAqEnabled(enabled); + synchronized (mSoundProfileLock) { + try { + if (mMediaQuality != null) { + if (mMediaQuality.isAutoAqSupported()) { + mMediaQuality.setAutoAqEnabled(enabled); + } } + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto sound quality", e); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set auto sound quality", e); } - } + }); } @GuardedBy("mSoundProfileLock") diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0ea9af4b9c38..e1e8fc231dda 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1712,15 +1712,19 @@ public class UserManagerService extends IUserManager.Stub { @Override public int getCredentialOwnerProfile(@UserIdInt int userId) { checkManageUsersPermission("get the credential owner"); - if (!mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) { - synchronized (mUsersLock) { - UserInfo profileParent = getProfileParentLU(userId); - if (profileParent != null) { - return profileParent.id; + final long identity = Binder.clearCallingIdentity(); + try { + if (!mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) { + synchronized (mUsersLock) { + UserInfo profileParent = getProfileParentLU(userId); + if (profileParent != null) { + return profileParent.id; + } } } + } finally { + Binder.restoreCallingIdentity(identity); } - return userId; } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index ac19ea12c6a4..fbf81b9accad 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1541,8 +1541,19 @@ public class PermissionManagerService extends IPermissionManager.Stub { } final AttributionSource resolvedAttributionSource = accessorSource.withPackageName(resolvedAccessorPackageName); - final int opMode = appOpsManager.unsafeCheckOpRawNoThrow(op, - resolvedAttributionSource); + // Avoid checking the first attr in the chain in some cases for consistency with + // checks for data delivery. + // In particular, for chains of 2 or more, when skipProxyOperation is true, the + // for data delivery implementation does not actually check the first link in the + // chain. If the attribution is just a singleReceiverFromDatasource, this + // exemption does not apply, since it does not go through proxyOp flow, and the top + // of the chain is actually removed above. + // Skipping the check avoids situations where preflight checks fail since the data + // source itself does not have the op (e.g. audioserver). + final int opMode = (skipProxyOperation && !singleReceiverFromDatasource) ? + AppOpsManager.MODE_ALLOWED : + appOpsManager.unsafeCheckOpRawNoThrow(op, resolvedAttributionSource); + final AttributionSource next = accessorSource.getNext(); if (!selfAccess && opMode == AppOpsManager.MODE_ALLOWED && next != null) { final String resolvedNextPackageName = resolvePackageName(context, next); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 8cf0481b1dc3..e8843ac214ec 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -520,32 +520,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { private WindowWakeUpPolicy mWindowWakeUpPolicy; - /** - * The three variables below are used for custom power key gesture detection in - * PhoneWindowManager. They are used to detect when the power button has been double pressed - * and, when it does happen, makes the behavior overrideable by the app. - * - * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection - * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the - * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result, - * overriding the double tap behavior requires custom gesture detection here that mimics the - * logic in {@link com.android.server.GestureLauncherService}. - * - * Long-term, it would be beneficial to move all power gesture detection to - * {@link PowerKeyRule} so that this custom logic isn't required. - */ - // Time of last power down event. - private long mLastPowerDown; - - // Number of power button events consecutively triggered (within a specific timeout threshold). - private int mPowerButtonConsecutiveTaps = 0; - - // Whether a double tap of the power button has been detected. - volatile boolean mDoubleTapPowerDetected; - - // Runnable that is queued on a delay when the first power keyDown event is sent to the app. - private Runnable mPowerKeyDelayedRunnable = null; - boolean mSafeMode; // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key. @@ -1135,10 +1109,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { || handledByPowerManager || isKeyGestureTriggered || mKeyCombinationManager.isPowerKeyIntercepted(); - if (overridePowerKeyBehaviorInFocusedWindow()) { - mPowerKeyHandled |= mDoubleTapPowerDetected; - } - if (!mPowerKeyHandled) { if (!interactive) { wakeUpFromWakeKey(event); @@ -2785,18 +2755,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } - // TODO(b/380433365): Remove deferring single power press action when refactoring. - if (overridePowerKeyBehaviorInFocusedWindow()) { - mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); - mDeferredKeyActionExecutor.queueKeyAction( - KEYCODE_POWER, - downTime, - () -> { - powerPress(downTime, 1 /*count*/, displayId); - }); - } else { - powerPress(downTime, 1 /*count*/, displayId); - } + powerPress(downTime, 1 /*count*/, displayId); } @Override @@ -2827,17 +2786,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { - if (overridePowerKeyBehaviorInFocusedWindow()) { - mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); - mDeferredKeyActionExecutor.queueKeyAction( - KEYCODE_POWER, - downTime, - () -> { - powerPress(downTime, count, displayId); - }); - } else { - powerPress(downTime, count, displayId); - } + powerPress(downTime, count, displayId); } @Override @@ -3614,12 +3563,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER - && event.getAction() == KeyEvent.ACTION_UP - && mDoubleTapPowerDetected) { - mDoubleTapPowerDetected = false; - } - return needToConsumeKey ? keyConsumed : keyNotConsumed; } @@ -4117,8 +4060,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } - case KeyEvent.KEYCODE_POWER: - return interceptPowerKeyBeforeDispatching(focusedToken, event); case KeyEvent.KEYCODE_SCREENSHOT: if (firstDown) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); @@ -4174,8 +4115,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } - case KeyEvent.KEYCODE_POWER: - return interceptPowerKeyBeforeDispatching(focusedToken, event); } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -4193,90 +4132,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return (metaState & KeyEvent.META_META_ON) != 0; } - /** - * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER - * KeyEvents. - * - * @return true if intercepting the key, false if sending to app. - */ - private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) { - if (!overridePowerKeyBehaviorInFocusedWindow()) { - //Flag disabled: intercept the power key and do not send to app. - return true; - } - if (event.getKeyCode() != KEYCODE_POWER) { - Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent " - + "with key code: " + event.getKeyCode()); - return false; - } - - // Intercept keys (don't send to app) for 3x, 4x, 5x gestures) - if (mPowerButtonConsecutiveTaps > DOUBLE_POWER_TAP_COUNT_THRESHOLD) { - setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); - return true; - } - - // UP key; just reuse the original decision. - if (event.getAction() == KeyEvent.ACTION_UP) { - final Set<Integer> consumedKeys = mConsumedKeysForDevice.get(event.getDeviceId()); - return consumedKeys != null - && consumedKeys.contains(event.getKeyCode()); - } - - KeyInterceptionInfo info = - mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken); - - if (info == null || !mButtonOverridePermissionChecker.canWindowOverridePowerKey(mContext, - info.windowOwnerUid, info.inputFeaturesFlags)) { - // The focused window does not have the permission to override power key behavior. - if (DEBUG_INPUT) { - String interceptReason = ""; - if (info == null) { - interceptReason = "Window is null"; - } else if (!mButtonOverridePermissionChecker.canAppOverrideSystemKey(mContext, - info.windowOwnerUid)) { - interceptReason = "Application does not have " - + "OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission"; - } else { - interceptReason = "Window does not have inputFeatureFlag set"; - } - - Log.d(TAG, TextUtils.formatSimple("Intercepting KEYCODE_POWER event. action=%d, " - + "eventTime=%d to window=%s. interceptReason=%s. " - + "mDoubleTapPowerDetected=%b", - event.getAction(), event.getEventTime(), (info != null) - ? info.windowTitle : "null", interceptReason, - mDoubleTapPowerDetected)); - } - // Intercept the key (i.e. do not send to app) - setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); - return true; - } - - if (DEBUG_INPUT) { - Log.d(TAG, TextUtils.formatSimple("Sending KEYCODE_POWER to app. action=%d, " - + "eventTime=%d to window=%s. mDoubleTapPowerDetected=%b", - event.getAction(), event.getEventTime(), info.windowTitle, - mDoubleTapPowerDetected)); - } - - if (!mDoubleTapPowerDetected) { - //Single press: post a delayed runnable for the single press power action that will be - // called if it's not cancelled by a double press. - final var downTime = event.getDownTime(); - mPowerKeyDelayedRunnable = () -> - setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, downTime); - mHandler.postDelayed(mPowerKeyDelayedRunnable, POWER_MULTI_PRESS_TIMEOUT_MILLIS); - } else if (mPowerKeyDelayedRunnable != null) { - //Double press detected: cancel the single press runnable. - mHandler.removeCallbacks(mPowerKeyDelayedRunnable); - mPowerKeyDelayedRunnable = null; - } - - // Focused window has permission. Send to app. - return false; - } - @SuppressLint("MissingPermission") private void initKeyGestures() { if (!useKeyGestureEventHandler()) { @@ -4764,11 +4619,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } - if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { - handleUnhandledSystemKey(event); - return true; - } - if (useKeyGestureEventHandler()) { return false; } @@ -5595,12 +5445,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); - if (overridePowerKeyBehaviorInFocusedWindow()) { - result |= ACTION_PASS_TO_USER; - } else { - // Any activity on the power button stops the accessibility shortcut - result &= ~ACTION_PASS_TO_USER; - } + // Any activity on the power button stops the accessibility shortcut + result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down) { interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered); @@ -5862,35 +5708,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) { - if (overridePowerKeyBehaviorInFocusedWindow()) { - if (event.getRepeatCount() > 0 && !mHasFeatureWatch) { - return; - } - if (mGestureLauncherService != null) { - mGestureLauncherService.processPowerKeyDown(event); - } - - if (detectDoubleTapPower(event)) { - mDoubleTapPowerDetected = true; - - // Copy of the event for handler in case the original event gets recycled. - KeyEvent eventCopy = KeyEvent.obtain(event); - mDeferredKeyActionExecutor.queueKeyAction( - KeyEvent.KEYCODE_POWER, - eventCopy.getEventTime(), - () -> { - if (!handleCameraGesture(eventCopy, interactive)) { - mSingleKeyGestureDetector.interceptKey( - eventCopy, interactive, defaultDisplayOn); - } else { - mSingleKeyGestureDetector.reset(); - } - eventCopy.recycle(); - }); - return; - } - } - mPowerKeyHandled = handleCameraGesture(event, interactive); if (mPowerKeyHandled) { // handled by camera gesture. @@ -5902,26 +5719,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } - private boolean detectDoubleTapPower(KeyEvent event) { - //Watches use the SingleKeyGestureDetector for detecting multi-press gestures. - if (mHasFeatureWatch || event.getKeyCode() != KEYCODE_POWER - || event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() != 0) { - return false; - } - - final long powerTapInterval = event.getEventTime() - mLastPowerDown; - mLastPowerDown = event.getEventTime(); - if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) { - // Tap too slow for double press - mPowerButtonConsecutiveTaps = 1; - } else { - mPowerButtonConsecutiveTaps++; - } - - return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS - && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD; - } - // The camera gesture will be detected by GestureLauncherService. private boolean handleCameraGesture(KeyEvent event, boolean interactive) { // camera gesture. @@ -7779,12 +7576,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { null) == PERMISSION_GRANTED; } - - boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) { - return canAppOverrideSystemKey(context, uid) - && (inputFeaturesFlags & WindowManager.LayoutParams - .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; - } } private int getTargetDisplayIdForKeyEvent(KeyEvent event) { diff --git a/services/core/java/com/android/server/theming/ThemeSettingsManager.java b/services/core/java/com/android/server/theming/ThemeSettingsManager.java new file mode 100644 index 000000000000..94094a6f9603 --- /dev/null +++ b/services/core/java/com/android/server/theming/ThemeSettingsManager.java @@ -0,0 +1,166 @@ +/* + * 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.server.theming; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.ContentResolver; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsField; +import android.content.theming.ThemeSettingsUpdater; +import android.provider.Settings; +import android.telecom.Log; + +import com.android.internal.util.Preconditions; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +/** + * Manages the loading and saving of theme settings. This class handles the persistence of theme + * settings to and from the system settings. It utilizes a collection of {@link ThemeSettingsField} + * objects to represent individual theme setting fields. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +class ThemeSettingsManager { + private static final String TAG = ThemeSettingsManager.class.getSimpleName(); + static final String TIMESTAMP_FIELD = "_applied_timestamp"; + private final ThemeSettingsField<?, ?>[] mFields; + private final ThemeSettings mDefaults; + + /** + * Constructs a new {@code ThemeSettingsManager} with the specified default settings. + * + * @param defaults The default theme settings to use. + */ + ThemeSettingsManager(ThemeSettings defaults) { + mDefaults = defaults; + mFields = ThemeSettingsField.getFields(defaults); + } + + /** + * Loads the theme settings for the specified user. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @return The loaded {@link ThemeSettings}. + */ + @NonNull + ThemeSettings loadSettings(@UserIdInt int userId, ContentResolver contentResolver) { + String jsonString = Settings.Secure.getStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId); + + JSONObject userSettings; + + try { + userSettings = new JSONObject(jsonString == null ? "" : jsonString); + } catch (JSONException e) { + userSettings = new JSONObject(); + } + + ThemeSettingsUpdater updater = ThemeSettings.updater(); + + for (ThemeSettingsField<?, ?> field : mFields) { + field.fromJSON(userSettings, updater); + } + + return updater.toThemeSettings(mDefaults); + } + + /** + * Saves the specified theme settings for the given user. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @param newSettings The {@link ThemeSettings} to save. + */ + void replaceSettings(@UserIdInt int userId, ContentResolver contentResolver, + ThemeSettings newSettings) throws RuntimeException { + Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings"); + + JSONObject jsonSettings = new JSONObject(); + + + for (ThemeSettingsField<?, ?> field : mFields) { + field.toJSON(newSettings, jsonSettings); + } + + // user defined timestamp should be ignored. Storing new timestamp. + try { + jsonSettings.put(TIMESTAMP_FIELD, System.currentTimeMillis()); + } catch (JSONException e) { + Log.w(TAG, "Error saving timestamp: " + e.getMessage()); + } + + String jsonString = jsonSettings.toString(); + + Settings.Secure.putStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, jsonString, userId); + } + + /** + * Saves the specified theme settings for the given user, while preserving unrelated existing + * properties. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @param newSettings The {@link ThemeSettings} to save. + */ + void updateSettings(@UserIdInt int userId, ContentResolver contentResolver, + ThemeSettings newSettings) throws JSONException, RuntimeException { + Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings"); + + String existingJsonString = Settings.Secure.getStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId); + + JSONObject existingJson; + try { + existingJson = new JSONObject(existingJsonString == null ? "{}" : existingJsonString); + } catch (JSONException e) { + existingJson = new JSONObject(); + } + + JSONObject newJson = new JSONObject(); + for (ThemeSettingsField<?, ?> field : mFields) { + field.toJSON(newSettings, newJson); + } + + // user defined timestamp should be ignored. Storing new timestamp. + try { + newJson.put(TIMESTAMP_FIELD, System.currentTimeMillis()); + } catch (JSONException e) { + Log.w(TAG, "Error saving timestamp: " + e.getMessage()); + } + + // Merge the new settings with the existing settings + Iterator<String> keys = newJson.keys(); + while (keys.hasNext()) { + String key = keys.next(); + existingJson.put(key, newJson.get(key)); + } + + String mergedJsonString = existingJson.toString(); + + Settings.Secure.putStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mergedJsonString, userId); + } +} diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 274175aa71ba..a0979fa5ee7e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -747,13 +747,22 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (connection == null) { return false; } + boolean isWallpaperDesktopExperienceEnabled = isDeviceEligibleForDesktopExperienceWallpaper( + mContext); + boolean isLiveWallpaperSupportedInDesktopExperience = + mContext.getResources().getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); // Non image wallpaper. if (connection.mInfo != null) { + if (isWallpaperDesktopExperienceEnabled + && !isLiveWallpaperSupportedInDesktopExperience) { + return false; + } return connection.mInfo.supportsMultipleDisplays(); } // Image wallpaper - if (isDeviceEligibleForDesktopExperienceWallpaper(mContext)) { + if (isWallpaperDesktopExperienceEnabled) { return mWallpaperCropper.isWallpaperCompatibleForDisplay(displayId, connection.mWallpaper); } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 7da4beb95114..5eceb6490256 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -95,6 +95,7 @@ import android.os.UserHandle; import android.service.voice.VoiceInteractionManagerInternal; import android.util.Slog; import android.view.RemoteAnimationDefinition; +import android.window.DesktopModeFlags; import android.window.SizeConfigurationBuckets; import android.window.TransitionInfo; @@ -1281,7 +1282,7 @@ class ActivityClientController extends IActivityClientController.Stub { } } - private static void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) { + private void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) { final int targetWindowingMode; if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) { final int restoreWindowingMode = requester.getRequestedOverrideWindowingMode(); @@ -1294,7 +1295,13 @@ class ActivityClientController extends IActivityClientController.Stub { requester.getParent().mRemoteToken.toWindowContainerToken(); } else { targetWindowingMode = requester.mMultiWindowRestoreWindowingMode; - requester.restoreWindowingMode(); + if (DesktopModeFlags.ENABLE_REQUEST_FULLSCREEN_BUGFIX.isTrue() + && targetWindowingMode == WINDOWING_MODE_PINNED) { + final ActivityRecord r = requester.topRunningActivity(); + enterPictureInPictureMode(r.token, r.pictureInPictureArgs); + } else { + requester.restoreWindowingMode(); + } } if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { requester.setBounds(null); diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java index 9d9c5ceb57d6..11fb229bb93d 100644 --- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java @@ -26,6 +26,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; @@ -166,7 +167,7 @@ public abstract class DisplayAreaPolicy { .all() .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, - TYPE_KEYGUARD_DIALOG, TYPE_WALLPAPER) + TYPE_KEYGUARD_DIALOG, TYPE_WALLPAPER, TYPE_VOLUME_OVERLAY) .build()); } if (USE_DISPLAY_AREA_FOR_FULLSCREEN_MAGNIFICATION) { diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 53681f950c8e..f2bc909bd64b 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -445,14 +445,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { if (controlTarget != null) { final boolean imeAnimating = Flags.reportAnimatingInsetsTypes() && (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0; - ImeTracker.forLogging().onProgress(statsToken, + final boolean imeVisible = + controlTarget.isRequestedVisible(WindowInsets.Type.ime()) || imeAnimating; + final var finalStatsToken = statsToken != null ? statsToken + : ImeTracker.forLogging().onStart( + imeVisible ? ImeTracker.TYPE_SHOW : ImeTracker.TYPE_HIDE, + ImeTracker.ORIGIN_SERVER, + SoftInputShowHideReason.IME_REQUESTED_CHANGED_LISTENER, + false /* fromUser */); + ImeTracker.forLogging().onProgress(finalStatsToken, ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY); mDisplayContent.mWmService.mH.post(() -> { - ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.forLogging().onProgress(finalStatsToken, ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER); - imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), - controlTarget.isRequestedVisible(WindowInsets.Type.ime()) - || imeAnimating, statsToken); + imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), imeVisible, + finalStatsToken); }); } else { ImeTracker.forLogging().onFailed(statsToken, diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java index c3649fe98056..709f491a3bdc 100644 --- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java @@ -72,7 +72,6 @@ class TaskChangeNotificationController { private final Handler mHandler; // Task stack change listeners in a remote process. - @GuardedBy("mRemoteTaskStackListeners") private final RemoteCallbackList<ITaskStackListener> mRemoteTaskStackListeners = new RemoteCallbackList<>(); @@ -311,9 +310,7 @@ class TaskChangeNotificationController { } } } else if (listener != null) { - synchronized (mRemoteTaskStackListeners) { - mRemoteTaskStackListeners.register(listener); - } + mRemoteTaskStackListeners.register(listener); } } @@ -323,24 +320,20 @@ class TaskChangeNotificationController { mLocalTaskStackListeners.remove(listener); } } else if (listener != null) { - synchronized (mRemoteTaskStackListeners) { - mRemoteTaskStackListeners.unregister(listener); - } + mRemoteTaskStackListeners.unregister(listener); } } private void forAllRemoteListeners(TaskStackConsumer callback, Message message) { - synchronized (mRemoteTaskStackListeners) { - for (int i = mRemoteTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) { - try { - // Make a one-way callback to the listener - callback.accept(mRemoteTaskStackListeners.getBroadcastItem(i), message); - } catch (RemoteException e) { - // Handled by the RemoteCallbackList. - } + for (int i = mRemoteTaskStackListeners.beginBroadcast() - 1; i >= 0; i--) { + try { + // Make a one-way callback to the listener + callback.accept(mRemoteTaskStackListeners.getBroadcastItem(i), message); + } catch (RemoteException e) { + // Handled by the RemoteCallbackList. } - mRemoteTaskStackListeners.finishBroadcast(); } + mRemoteTaskStackListeners.finishBroadcast(); } private void forAllLocalListeners(TaskStackConsumer callback, Message message) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 5f2a2ad7f0eb..8ed93276d646 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -371,7 +371,7 @@ public abstract class WindowManagerInternal { * @param statsToken the token tracking the current IME request. */ void onImeRequestedChanged(IBinder windowToken, boolean imeVisible, - @Nullable ImeTracker.Token statsToken); + @NonNull ImeTracker.Token statsToken); } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 00a437cc31f9..c23dabcd2a48 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -67,7 +67,6 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; @@ -99,7 +98,6 @@ import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ER import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.window.WindowProviderService.isWindowProviderService; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_BOOT; @@ -9241,25 +9239,6 @@ public class WindowManagerService extends IWindowManager.Stub + "' because it isn't a trusted overlay"); return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } - - // You need OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission to be able - // to set INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS. - if (overridePowerKeyBehaviorInFocusedWindow() - && (inputFeatures - & INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) - != 0) { - final int powerPermissionResult = - mContext.checkPermission( - permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW, - callingPid, - callingUid); - if (powerPermissionResult != PackageManager.PERMISSION_GRANTED) { - throw new IllegalArgumentException( - "Cannot use INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS from" + windowName - + " because it doesn't have the" - + " OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission"); - } - } return inputFeatures; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index a03b765cae6a..0bd27d10559c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5507,10 +5507,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP || mKeyInterceptionInfo.layoutParamsPrivateFlags != mAttrs.privateFlags || mKeyInterceptionInfo.layoutParamsType != mAttrs.type || mKeyInterceptionInfo.windowTitle != getWindowTag() - || mKeyInterceptionInfo.windowOwnerUid != getOwningUid() - || mKeyInterceptionInfo.inputFeaturesFlags != mAttrs.inputFeatures) { + || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()) { mKeyInterceptionInfo = new KeyInterceptionInfo(mAttrs.type, mAttrs.privateFlags, - getWindowTag().toString(), getOwningUid(), mAttrs.inputFeatures); + getWindowTag().toString(), getOwningUid()); } return mKeyInterceptionInfo; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index e29511564cea..ee7c9368f897 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -2926,6 +2926,12 @@ static void nativeReloadDeviceAliases(JNIEnv* env, jobject nativeImplObj) { InputReaderConfiguration::Change::DEVICE_ALIAS); } +static jstring nativeGetSysfsRootPath(JNIEnv* env, jobject nativeImplObj, jint deviceId) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + const auto path = im->getInputManager()->getReader().getSysfsRootPath(deviceId); + return path.empty() ? nullptr : env->NewStringUTF(path.c_str()); +} + static void nativeSysfsNodeChanged(JNIEnv* env, jobject nativeImplObj, jstring path) { ScopedUtfChars sysfsNodePathChars(env, path); const std::string sysfsNodePath = sysfsNodePathChars.c_str(); @@ -3388,6 +3394,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"getBatteryDevicePath", "(I)Ljava/lang/String;", (void*)nativeGetBatteryDevicePath}, {"reloadKeyboardLayouts", "()V", (void*)nativeReloadKeyboardLayouts}, {"reloadDeviceAliases", "()V", (void*)nativeReloadDeviceAliases}, + {"getSysfsRootPath", "(I)Ljava/lang/String;", (void*)nativeGetSysfsRootPath}, {"sysfsNodeChanged", "(Ljava/lang/String;)V", (void*)nativeSysfsNodeChanged}, {"dump", "()Ljava/lang/String;", (void*)nativeDump}, {"monitor", "()V", (void*)nativeMonitor}, diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index d984fcc599cb..964826d1ee73 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -23903,10 +23903,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { UserHandle.USER_ALL); synchronized (getLockObject()) { - final EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin(null, - MANAGE_DEVICE_POLICY_MTE, callerPackageName, caller.getUserId()); - final Integer policyFromAdmin = mDevicePolicyEngine.getGlobalPolicySetByAdmin( - PolicyDefinition.MEMORY_TAGGING, admin); + final Integer policyFromAdmin = mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.MEMORY_TAGGING, UserHandle.USER_ALL); + return (policyFromAdmin != null ? policyFromAdmin : DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY); } diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 7a6bd75e5893..f864b6b8c768 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -31,6 +31,13 @@ flag { } flag { + namespace: "system_performance" + name: "enable_theme_service" + description: "Switches from SystemUi's ThemeOverlayController to Server's ThemeService." + bug: "333694176" +} + +flag { name: "allow_removing_vpn_service" namespace: "wear_frameworks" description: "Allow removing VpnManagerService" diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index 1bb395441587..410539c8c5d0 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -431,7 +431,7 @@ private constructor( companion object { private val LOG_TAG = AccessPolicy::class.java.simpleName - internal const val VERSION_LATEST = 16 + internal const val VERSION_LATEST = 17 private const val TAG_ACCESS = "access" private const val TAG_DEFAULT_PERMISSION_GRANT = "default-permission-grant" diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt index 5d8886a9e2d9..022b811d9ac8 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt @@ -17,8 +17,10 @@ package com.android.server.permission.access.permission import android.Manifest +import android.health.connect.HealthPermissions import android.os.Build import android.util.Slog +import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.MutateStateScope import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports import com.android.server.permission.access.util.andInv @@ -64,7 +66,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { ", version: $version, user: $userId", ) upgradeAuralVisualMediaPermissions(packageState, userId) - upgradeBodySensorPermissions(packageState, userId) + upgradeBodySensorBackgroundPermissions(packageState, userId) } // TODO Enable isAtLeastU check, when moving subsystem to mainline. if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) { @@ -75,6 +77,16 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { ) upgradeUserSelectedVisualMediaPermission(packageState, userId) } + // TODO Enable isAtLeastB check, when moving subsystem to mainline. + if (version <= 16 /*&& SdkLevel.isAtLeastB()*/) { + Slog.v( + LOG_TAG, + "Upgrading body sensor / read heart rate permissions for package: $packageName" + + ", version: $version, user: $userId", + ) + upgradeBodySensorReadHeartRatePermissions(packageState, userId) + } + // Add a new upgrade step: if (packageVersion <= LATEST_VERSION) { .... } // Also increase LATEST_VERSION } @@ -182,7 +194,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { } } - private fun MutateStateScope.upgradeBodySensorPermissions( + private fun MutateStateScope.upgradeBodySensorBackgroundPermissions( packageState: PackageState, userId: Int, ) { @@ -256,6 +268,112 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { } } + /** + * Upgrade permissions based on the body sensors and health permissions status. + * + * Starting in BAKLAVA, the BODY_SENSORS and BODY_SENSORS_BACKGROUND permissions are being + * replaced by the READ_HEART_RATE and READ_HEALTH_DATA_IN_BACKGROUND permissions respectively. + * To ensure that older apps can continue using BODY_SENSORS without breaking we need to keep + * their permission state in sync with the new health permissions. + * + * The approach we take is to be as conservative as possible. This means if either permission is + * not granted, then we want to ensure that both end up not granted to force the user to + * re-grant with the expanded scope. + */ + private fun MutateStateScope.upgradeBodySensorReadHeartRatePermissions( + packageState: PackageState, + userId: Int, + ) { + val androidPackage = packageState.androidPackage!! + if (androidPackage.targetSdkVersion >= Build.VERSION_CODES.BAKLAVA) { + return + } + + // First sync BODY_SENSORS and READ_HEART_RATE, if required. + val isBodySensorsRequested = + Manifest.permission.BODY_SENSORS in androidPackage.requestedPermissions + val isReadHeartRateRequested = + HealthPermissions.READ_HEART_RATE in androidPackage.requestedPermissions + var isBodySensorsGranted = + isPermissionGranted(packageState, userId, Manifest.permission.BODY_SENSORS) + if (isBodySensorsRequested && isReadHeartRateRequested) { + val isReadHeartRateGranted = + isPermissionGranted(packageState, userId, HealthPermissions.READ_HEART_RATE) + if (isBodySensorsGranted != isReadHeartRateGranted) { + if (isBodySensorsGranted) { + if ( + revokeRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS, + ) + ) { + isBodySensorsGranted = false + } + } + if (isReadHeartRateGranted) { + revokeRuntimePermission(packageState, userId, HealthPermissions.READ_HEART_RATE) + } + } + } + + // Then check to ensure we haven't put the background/foreground permissions out of sync. + var isBodySensorsBackgroundGranted = + isPermissionGranted(packageState, userId, Manifest.permission.BODY_SENSORS_BACKGROUND) + // Background permission should not be granted without the foreground permission. + if (!isBodySensorsGranted && isBodySensorsBackgroundGranted) { + if ( + revokeRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + ) { + isBodySensorsBackgroundGranted = false + } + } + + // Finally sync BODY_SENSORS_BACKGROUND and READ_HEALTH_DATA_IN_BACKGROUND, if required. + val isBodySensorsBackgroundRequested = + Manifest.permission.BODY_SENSORS_BACKGROUND in androidPackage.requestedPermissions + val isReadHealthDataInBackgroundRequested = + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND in androidPackage.requestedPermissions + if (isBodySensorsBackgroundRequested && isReadHealthDataInBackgroundRequested) { + val isReadHealthDataInBackgroundGranted = + isPermissionGranted( + packageState, + userId, + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, + ) + if (isBodySensorsBackgroundGranted != isReadHealthDataInBackgroundGranted) { + if (isBodySensorsBackgroundGranted) { + revokeRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + ) + } + if (isReadHealthDataInBackgroundGranted) { + revokeRuntimePermission( + packageState, + userId, + HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND, + ) + } + } + } + } + + private fun GetStateScope.isPermissionGranted( + packageState: PackageState, + userId: Int, + permissionName: String, + ): Boolean { + val permissionFlags = + with(policy) { getPermissionFlags(packageState.appId, userId, permissionName) } + return PermissionFlags.isAppOpGranted(permissionFlags) + } + private fun MutateStateScope.grantRuntimePermission( packageState: PackageState, userId: Int, @@ -292,6 +410,47 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { with(policy) { setPermissionFlags(appId, userId, permissionName, flags) } } + /** + * Revoke a runtime permission for a given user from a given package. + * + * @return true if the permission was revoked, false otherwise. + */ + private fun MutateStateScope.revokeRuntimePermission( + packageState: PackageState, + userId: Int, + permissionName: String, + ): Boolean { + Slog.v( + LOG_TAG, + "Revoking runtime permission for package: ${packageState.packageName}, " + + "permission: $permissionName, userId: $userId", + ) + val permission = newState.systemState.permissions[permissionName]!! + if (packageState.getUserStateOrDefault(userId).isInstantApp && !permission.isInstant) { + return false + } + + val appId = packageState.appId + var flags = with(policy) { getPermissionFlags(appId, userId, permissionName) } + if (flags.hasAnyBit(MASK_SYSTEM_OR_POLICY_FIXED)) { + Slog.v( + LOG_TAG, + "Not allowed to revoke $permissionName to package ${packageState.packageName} " + + "for user $userId", + ) + return false + } + + val newFlags = + flags andInv + (PermissionFlags.RUNTIME_GRANTED or + MASK_USER_SETTABLE or + PermissionFlags.PREGRANT or + PermissionFlags.ROLE) + with(policy) { setPermissionFlags(appId, userId, permissionName, flags) } + return true + } + companion object { private val LOG_TAG = AppIdPermissionUpgrade::class.java.simpleName @@ -302,6 +461,17 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { PermissionFlags.POLICY_FIXED or PermissionFlags.SYSTEM_FIXED + private const val MASK_SYSTEM_OR_POLICY_FIXED = + PermissionFlags.SYSTEM_FIXED or PermissionFlags.POLICY_FIXED + + private const val MASK_USER_SETTABLE = + PermissionFlags.USER_SET or + PermissionFlags.USER_FIXED or + PermissionFlags.APP_OP_REVOKED or + PermissionFlags.ONE_TIME or + PermissionFlags.HIBERNATION or + PermissionFlags.USER_SELECTED + private val LEGACY_RESTRICTED_PERMISSIONS = indexedSetOf( Manifest.permission.ACCESS_BACKGROUND_LOCATION, diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index 59eb0c890af5..f8f08a51b375 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -64,6 +64,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ServiceInfo; +import android.content.res.Resources; import android.graphics.Color; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; @@ -1188,6 +1189,10 @@ public class WallpaperManagerServiceTests { @Test @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) public void setWallpaperComponent_systemAndLockWallpapers_multiDisplays_shouldHaveExpectedConnections() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(true).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); final int incompatibleDisplayId = 2; final int compatibleDisplayId = 3; setUpDisplays(Map.of( @@ -1227,6 +1232,71 @@ public class WallpaperManagerServiceTests { .isTrue(); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void isWallpaperCompatibleForDisplay_liveWallpaperSupported_desktopExperienceEnabled_shouldReturnTrue() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(true).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); + + final int displayId = 2; + setUpDisplays(Map.of( + DEFAULT_DISPLAY, true, + displayId, true)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); + + assertThat(mService.isWallpaperCompatibleForDisplay(displayId, + mService.mLastWallpaper.connection)).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void isWallpaperCompatibleForDisplay_liveWallpaperUnsupported_desktopExperienceEnabled_shouldReturnFalse() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(false).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); + + final int displayId = 2; + setUpDisplays(Map.of( + DEFAULT_DISPLAY, true, + displayId, true)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); + + assertThat(mService.isWallpaperCompatibleForDisplay(displayId, + mService.mLastWallpaper.connection)).isFalse(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void isWallpaperCompatibleForDisplay_liveWallpaperUnsupported_desktopExperienceDisabled_shouldReturnTrue() { + Resources resources = sContext.getResources(); + spyOn(resources); + doReturn(false).when(resources).getBoolean( + R.bool.config_isLiveWallpaperSupportedInDesktopExperience); + + final int displayId = 2; + setUpDisplays(Map.of( + DEFAULT_DISPLAY, true, + displayId, true)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(), + FLAG_SYSTEM | FLAG_LOCK, testUserId); + + // config_isLiveWallpaperSupportedInDesktopExperience is not used if the desktop experience + // flag for wallpaper is disabled. + assertThat(mService.isWallpaperCompatibleForDisplay(displayId, + mService.mLastWallpaper.connection)).isTrue(); + } + // Verify that after continue switch user from userId 0 to lastUserId, the wallpaper data for // non-current user must not bind to wallpaper service. private void verifyNoConnectionBeforeLastUser(int lastUserId) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index 53a2522d299e..f29708496940 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -340,7 +340,7 @@ public class BatteryUsageStatsRule implements TestRule { } private void before() { - BatteryUsageStats.DEBUG_INSTANCE_COUNT = true; + BatteryUsageStats.enableInstanceLeakDetection(); HandlerThread bgThread = new HandlerThread("bg thread"); bgThread.setUncaughtExceptionHandler((thread, throwable)-> { mThrowable = throwable; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 64e6d323bdfd..d3c3178f3513 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -347,6 +347,17 @@ test_module_config { include_filters: ["com.android.server.om."], } +test_module_config { + name: "FrameworksServicesTests_theme", + base: "FrameworksServicesTests", + test_suites: [ + "device-tests", + "automotive-tests", + ], + + include_filters: ["com.android.server.theming."], +} + // Used by contexthub TEST_MAPPING test_module_config { name: "FrameworksServicesTests_contexthub_presubmit", diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index cc0d5e4710d2..73e5f8232faf 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -1787,46 +1787,6 @@ public class GestureLauncherServiceTest { } /** - * If processPowerKeyDown is called instead of interceptPowerKeyDown (meaning the double tap - * gesture isn't performed), the emergency gesture is still launched. - */ - @Test - public void testProcessPowerKeyDown_fiveInboundPresses_emergencyGestureLaunches() { - enableCameraGesture(); - enableEmergencyGesture(); - - // First event - long eventTime = INITIAL_EVENT_TIME_MILLIS; - sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); - - //Second event; call processPowerKeyDown without calling interceptPowerKeyDown - final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; - eventTime += interval; - KeyEvent keyEvent = - new KeyEvent( - IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); - mGestureLauncherService.processPowerKeyDown(keyEvent); - - verify(mMetricsLogger, never()) - .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); - verify(mUiEventLogger, never()).log(any()); - - // Presses 3 and 4 should not trigger any gesture - for (int i = 0; i < 2; i++) { - eventTime += interval; - sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); - } - - // Fifth button press should still trigger the emergency flow - eventTime += interval; - sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); - - verify(mUiEventLogger, times(1)) - .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); - verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); - } - - /** * Helper method to trigger emergency gesture by pressing button for 5 times. * * @return last event time. diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index df77866b5e7f..0f418ab5d19c 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -88,6 +88,23 @@ public class AutoclickControllerTest { } } + public static class ScrollEventCaptor extends BaseEventStreamTransformation { + public MotionEvent scrollEvent; + public int eventCount = 0; + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (event.getAction() == MotionEvent.ACTION_SCROLL) { + if (scrollEvent != null) { + scrollEvent.recycle(); + } + scrollEvent = MotionEvent.obtain(event); + eventCount++; + } + super.onMotionEvent(event, rawEvent, policyFlags); + } + } + @Before public void setUp() { mTestableLooper = TestableLooper.get(this); @@ -918,6 +935,110 @@ public class AutoclickControllerTest { MotionEvent.BUTTON_PRIMARY); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void sendClick_updateLastCursorAndScrollAtThatLocation() { + // Set up event capturer to track scroll events. + ScrollEventCaptor scrollCaptor = new ScrollEventCaptor(); + mController.setNext(scrollCaptor); + + // Initialize controller with mouse event. + injectFakeMouseActionHoverMoveEvent(); + + // Mock the scroll panel. + AutoclickScrollPanel mockScrollPanel = mock(AutoclickScrollPanel.class); + mController.mAutoclickScrollPanel = mockScrollPanel; + + // Set click type to scroll. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL); + + // Set cursor position. + float expectedX = 75f; + float expectedY = 125f; + mController.mLastCursorX = expectedX; + mController.mLastCursorY = expectedY; + + // Trigger scroll action in up direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_UP, true); + + // Verify scroll event happens at last cursor location. + assertThat(scrollCaptor.scrollEvent).isNotNull(); + assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); + assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void handleScroll_generatesCorrectScrollEvents() { + ScrollEventCaptor scrollCaptor = new ScrollEventCaptor(); + mController.setNext(scrollCaptor); + + // Initialize controller. + injectFakeMouseActionHoverMoveEvent(); + + // Set cursor position. + final float expectedX = 100f; + final float expectedY = 200f; + mController.mLastCursorX = expectedX; + mController.mLastCursorY = expectedY; + + // Test UP direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_UP, true); + + // Verify basic event properties. + assertThat(scrollCaptor.eventCount).isEqualTo(1); + assertThat(scrollCaptor.scrollEvent).isNotNull(); + assertThat(scrollCaptor.scrollEvent.getAction()).isEqualTo(MotionEvent.ACTION_SCROLL); + assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); + assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); + + // Verify UP direction uses correct axis values. + float vScrollUp = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollUp = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(vScrollUp).isGreaterThan(0); + assertThat(hScrollUp).isEqualTo(0); + + // Test DOWN direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_DOWN, true); + + // Verify DOWN direction uses correct axis values. + assertThat(scrollCaptor.eventCount).isEqualTo(2); + float vScrollDown = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollDown = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(vScrollDown).isLessThan(0); + assertThat(hScrollDown).isEqualTo(0); + + // Test LEFT direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_LEFT, true); + + // Verify LEFT direction uses correct axis values. + assertThat(scrollCaptor.eventCount).isEqualTo(3); + float vScrollLeft = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollLeft = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(hScrollLeft).isGreaterThan(0); + assertThat(vScrollLeft).isEqualTo(0); + + // Test RIGHT direction. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_RIGHT, true); + + // Verify RIGHT direction uses correct axis values. + assertThat(scrollCaptor.eventCount).isEqualTo(4); + float vScrollRight = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); + float hScrollRight = scrollCaptor.scrollEvent.getAxisValue(MotionEvent.AXIS_HSCROLL); + assertThat(hScrollRight).isLessThan(0); + assertThat(vScrollRight).isEqualTo(0); + + // Verify scroll cursor position is preserved. + assertThat(scrollCaptor.scrollEvent.getX()).isEqualTo(expectedX); + assertThat(scrollCaptor.scrollEvent.getY()).isEqualTo(expectedY); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java index aed68a5dc7b5..113e039d9a43 100644 --- a/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/media/LegacyDeviceRouteControllerTest.java @@ -30,12 +30,17 @@ import android.media.AudioRoutesInfo; import android.media.IAudioRoutesObserver; import android.media.MediaRoute2Info; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.TextUtils; import com.android.internal.R; +import com.android.media.flags.Flags; import com.android.server.audio.AudioService; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; @@ -48,6 +53,7 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.Collection; +import java.util.List; @RunWith(Enclosed.class) public class LegacyDeviceRouteControllerTest { @@ -70,6 +76,11 @@ public class LegacyDeviceRouteControllerTest { @RunWith(JUnit4.class) public static class DefaultDeviceRouteValueTest { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private Context mContext; @Mock @@ -136,6 +147,23 @@ public class LegacyDeviceRouteControllerTest { .isTrue(); assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); } + + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_FIX_FOR_EMPTY_SYSTEM_ROUTES_CRASH) + @Test + public void getAvailableRoutes_matchesSelectedRoute() { + when(mResources.getText(R.string.default_audio_route_name)) + .thenReturn(DEFAULT_ROUTE_NAME); + + when(mAudioService.startWatchingRoutes(any())).thenReturn(null); + + LegacyDeviceRouteController deviceRouteController = + new LegacyDeviceRouteController( + mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener); + + MediaRoute2Info selectedRoute = deviceRouteController.getSelectedRoute(); + assertThat(deviceRouteController.getAvailableRoutes()) + .isEqualTo(List.of(selectedRoute)); + } } @RunWith(Parameterized.class) diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java new file mode 100644 index 000000000000..38cbcf37f88c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java @@ -0,0 +1,99 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorBoth; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorBothTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private FieldColorBoth mFieldColorBoth; + + @Before + public void setup() { + mFieldColorBoth = new FieldColorBoth("colorBoth", ThemeSettingsUpdater::colorBoth, + ThemeSettings::colorBoth, DEFAULTS); + } + + @Test + public void parse_validColorBoth_returnsTrue() { + Boolean parsedValue = mFieldColorBoth.parse("1"); + assertThat(parsedValue).isTrue(); + } + + @Test + public void parse_validColorBoth_returnsFalse() { + Boolean parsedValue = mFieldColorBoth.parse("0"); + assertThat(parsedValue).isFalse(); + } + + @Test + public void parse_invalidColorBoth_returnsNull() { + Boolean parsedValue = mFieldColorBoth.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_true_returnsTrueString() { + String serializedValue = mFieldColorBoth.serialize(true); + assertThat(serializedValue).isEqualTo("1"); + } + + @Test + public void serialize_false_returnsFalseString() { + String serializedValue = mFieldColorBoth.serialize(false); + assertThat(serializedValue).isEqualTo("0"); + } + + @Test + public void validate_true_returnsTrue() { + assertThat(mFieldColorBoth.validate(true)).isTrue(); + } + + @Test + public void validate_false_returnsTrue() { + assertThat(mFieldColorBoth.validate(false)).isTrue(); + } + + @Test + public void getFieldType_returnsBooleanClass() { + Truth.assertThat(mFieldColorBoth.getFieldType()).isEqualTo(Boolean.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColorBoth.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColorBoth.getDefaultValue()).isEqualTo(DEFAULTS.colorBoth()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java new file mode 100644 index 000000000000..32df3684a81d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java @@ -0,0 +1,103 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorIndex; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorIndexTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldColorIndex mFieldColorIndex; + + @Before + public void setup() { + mFieldColorIndex = new FieldColorIndex("colorIndex", ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, DEFAULTS); + } + + @Test + public void parse_validColorIndex_returnsCorrectInteger() { + Integer parsedValue = mFieldColorIndex.parse("10"); + assertThat(parsedValue).isEqualTo(10); + } + + @Test + public void parse_negativeColorIndex_returnsCorrectInteger() { + Integer parsedValue = mFieldColorIndex.parse("-1"); + assertThat(parsedValue).isEqualTo(-1); + } + + @Test + public void parse_invalidColorIndex_returnsNull() { + Integer parsedValue = mFieldColorIndex.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validColorIndex_returnsCorrectString() { + String serializedValue = mFieldColorIndex.serialize(15); + assertThat(serializedValue).isEqualTo("15"); + } + + @Test + public void serialize_negativeColorIndex_returnsCorrectString() { + String serializedValue = mFieldColorIndex.serialize(-1); + assertThat(serializedValue).isEqualTo("-1"); + } + + @Test + public void validate_validColorIndex_returnsTrue() { + assertThat(mFieldColorIndex.validate(5)).isTrue(); + } + + @Test + public void validate_negativeColorIndex_returnsTrue() { + assertThat(mFieldColorIndex.validate(-1)).isTrue(); + } + + @Test + public void validate_invalidColorIndex_returnsFalse() { + assertThat(mFieldColorIndex.validate(-2)).isFalse(); + } + + @Test + public void getFieldType_returnsIntegerClass() { + assertThat(mFieldColorIndex.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + assertThat(mFieldColorIndex.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + assertThat(mFieldColorIndex.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java new file mode 100644 index 000000000000..06edfa862d9c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java @@ -0,0 +1,94 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorSource; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + + +@RunWith(JUnit4.class) +public class FieldColorSourceTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private FieldColorSource mFieldColorSource; + + @Before + public void setup() { + mFieldColorSource = new FieldColorSource("colorSource", ThemeSettingsUpdater::colorSource, + ThemeSettings::colorSource, DEFAULTS); + } + + @Test + public void parse_validColorSource_returnsSameString() { + String validColorSource = "home_wallpaper"; + String parsedValue = mFieldColorSource.parse(validColorSource); + assertThat(parsedValue).isEqualTo(validColorSource); + } + + @Test + public void serialize_validColorSource_returnsSameString() { + String validColorSource = "lock_wallpaper"; + String serializedValue = mFieldColorSource.serialize(validColorSource); + assertThat(serializedValue).isEqualTo(validColorSource); + } + + @Test + public void validate_preset_returnsTrue() { + assertThat(mFieldColorSource.validate("preset")).isTrue(); + } + + @Test + public void validate_homeWallpaper_returnsTrue() { + assertThat(mFieldColorSource.validate("home_wallpaper")).isTrue(); + } + + @Test + public void validate_lockWallpaper_returnsTrue() { + assertThat(mFieldColorSource.validate("lock_wallpaper")).isTrue(); + } + + @Test + public void validate_invalidColorSource_returnsFalse() { + assertThat(mFieldColorSource.validate("invalid")).isFalse(); + } + + @Test + public void getFieldType_returnsStringClass() { + Truth.assertThat(mFieldColorSource.getFieldType()).isEqualTo(String.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColorSource.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColorSource.getDefaultValue()).isEqualTo(DEFAULTS.colorSource()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java new file mode 100644 index 000000000000..54c4b29a5063 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java @@ -0,0 +1,107 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColor; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldColor mFieldColor; + + @Before + public void setup() { + // Default to blue + mFieldColor = new FieldColor("accentColor", ThemeSettingsUpdater::accentColor, + ThemeSettings::accentColor, DEFAULTS); + } + + @Test + public void parse_validColor_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("FF0000FF"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } @Test + public void parse_validColorLowercase_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("ff0000ff"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } + + @Test + public void parse_validColorNoAlpha_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("0000ff"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } + + + @Test + public void parse_invalidColor_returnsNull() { + Integer parsedValue = mFieldColor.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void parse_nullColor_returnsNull() { + Integer parsedValue = mFieldColor.parse(null); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validColor_returnsCorrectString() { + String serializedValue = mFieldColor.serialize(0xFFFF0000); // Red + assertThat(serializedValue).isEqualTo("ffff0000"); + } + + @Test + public void serialize_zeroColor_returnsZeroString() { + String serializedValue = mFieldColor.serialize(0); + assertThat(serializedValue).isEqualTo("0"); + } + + @Test + public void validate_validColor_returnsTrue() { + assertThat(mFieldColor.validate(0xFF00FF00)).isTrue(); // Green + } + + @Test + public void getFieldType_returnsIntegerClass() { + Truth.assertThat(mFieldColor.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColor.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColor.getDefaultValue()).isEqualTo(DEFAULTS.accentColor()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java new file mode 100644 index 000000000000..09d71292fcf6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java @@ -0,0 +1,86 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldThemeStyle; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldThemeStyleTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldThemeStyle mFieldThemeStyle; + + @Before + public void setup() { + mFieldThemeStyle = new FieldThemeStyle("themeStyle", ThemeSettingsUpdater::themeStyle, + ThemeSettings::themeStyle, DEFAULTS); + } + + @Test + public void parse_validThemeStyle_returnsCorrectStyle() { + Integer parsedValue = mFieldThemeStyle.parse("EXPRESSIVE"); + assertThat(parsedValue).isEqualTo(ThemeStyle.EXPRESSIVE); + } + + @Test + public void parse_invalidThemeStyle_returnsNull() { + Integer parsedValue = mFieldThemeStyle.parse("INVALID"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validThemeStyle_returnsCorrectString() { + String serializedValue = mFieldThemeStyle.serialize(ThemeStyle.SPRITZ); + assertThat(serializedValue).isEqualTo("SPRITZ"); + } + + @Test + public void validate_validThemeStyle_returnsTrue() { + assertThat(mFieldThemeStyle.validate(ThemeStyle.TONAL_SPOT)).isTrue(); + } + + @Test + public void validate_invalidThemeStyle_returnsFalse() { + assertThat(mFieldThemeStyle.validate(-1)).isFalse(); + } + + @Test + public void getFieldType_returnsIntegerClass() { + assertThat(mFieldThemeStyle.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + assertThat(mFieldThemeStyle.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + assertThat(mFieldThemeStyle.getDefaultValue()).isEqualTo(DEFAULTS.themeStyle()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING new file mode 100644 index 000000000000..d8d73444f6ce --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests_theme" + } + ] +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java new file mode 100644 index 000000000000..0dc267a8059f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java @@ -0,0 +1,169 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsField; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +@RunWith(JUnit4.class) +public class ThemeSettingsFieldTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private ThemeSettingsUpdater mUpdater; + + @Before + public void setup() { + mUpdater = ThemeSettings.updater(); + } + + @Test + public void testFromJSON_validValue_setsValue() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "5"); + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(5); + } + + @Test + public void testFromJSON_nullValue_setsDefault() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", + JSONObject.NULL); // Using JSONObject.NULL is how you should indicate null in JSON + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void testFromJSON_invalidValue_setsDefault() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "abc"); // Invalid value + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void testToJSON_validValue_writesValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + ThemeSettings settings = new ThemeSettings(10, 0xFF123456, 0xFF654321, "home_wallpaper", + 0, true); + JSONObject json = new JSONObject(); + + field.toJSON(settings, json); + + assertThat(json.getString("testKey")).isEqualTo("10"); + } + + @Test + public void testDefaultValue_returnsGetDefault() { + TestThemeSettingsFieldInteger field = getSampleField(); + + assertThat(field.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void test_String_validValue_returnsParsedValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "123"); + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(123); + } + + @Test + public void test_String_invalidValue_returnsDefaultValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + // values < 0 are invalid + json.put("testKey", "-123"); + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + private TestThemeSettingsFieldInteger getSampleField() { + return new TestThemeSettingsFieldInteger("testKey", ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, DEFAULTS); + } + + + // Helper class for testing + private static class TestThemeSettingsFieldInteger extends ThemeSettingsField<Integer, String> { + TestThemeSettingsFieldInteger(String key, BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, ThemeSettings defaults) { + super(key, setter, getter, defaults); + } + + @Override + public Integer parse(String primitive) { + try { + return Integer.parseInt(primitive); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String serialize(Integer value) throws RuntimeException { + return value.toString(); + } + + @Override + public boolean validate(Integer value) { + return value > 0; + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java new file mode 100644 index 000000000000..44f8c73dec84 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java @@ -0,0 +1,114 @@ +/* + * 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.server.theming; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeStyle; +import android.provider.Settings; +import android.testing.TestableContext; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ThemeSettingsManagerTests { + private final int mUserId = 0; + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + + @Rule + public final TestableContext mContext = new TestableContext( + getInstrumentation().getTargetContext(), null); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private ContentResolver mContentResolver; + + + @Before + public void setup() { + mContentResolver = mContext.getContentResolver(); + } + + @Test + public void loadSettings_emptyJSON_returnsDefault() { + Settings.Secure.putStringForUser(mContentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, "{}", mUserId); + + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + ThemeSettings settings = manager.loadSettings(mUserId, mContentResolver); + + assertThat(settings.colorIndex()).isEqualTo(DEFAULTS.colorIndex()); + assertThat(settings.systemPalette()).isEqualTo(DEFAULTS.systemPalette()); + assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor()); + assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth()); + } + + @Test + public void replaceSettings_writesSettingsToProvider() throws Exception { + + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + + ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset", + ThemeStyle.MONOCHROMATIC, false); + manager.replaceSettings(mUserId, mContentResolver, newSettings); + + String settingsString = Settings.Secure.getStringForUser(mContentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mUserId); + JSONObject settingsJson = new JSONObject(settingsString); + assertThat(settingsJson.getString("android.theme.customization.color_index")).isEqualTo( + "3"); + assertThat(settingsJson.getString("android.theme.customization.system_palette")) + .isEqualTo("ff112233"); + assertThat(settingsJson.getString("android.theme.customization.accent_color")) + .isEqualTo("ff332211"); + assertThat(settingsJson.getString("android.theme.customization.color_source")) + .isEqualTo("preset"); + assertThat(settingsJson.getString("android.theme.customization.theme_style")) + .isEqualTo("MONOCHROMATIC"); + assertThat(settingsJson.getString("android.theme.customization.color_both")).isEqualTo("0"); + } + + @Test + public void updatesSettings_writesSettingsToProvider() throws Exception { + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + + ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset", + ThemeStyle.MONOCHROMATIC, false); + manager.updateSettings(mUserId, mContentResolver, newSettings); + + ThemeSettings loadedSettings = manager.loadSettings(mUserId, mContentResolver); + assertThat(loadedSettings.equals(newSettings)).isTrue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java new file mode 100644 index 000000000000..c417a4b571cb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java @@ -0,0 +1,108 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertNull; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ThemeSettingsTests { + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + + /** + * Test that the updater correctly sets all fields when they are provided. + */ + @Test + public void testUpdater_allFieldsSet() { + ThemeSettingsUpdater updater = ThemeSettings.updater() + .colorIndex(2) + .systemPalette(0xFFFF0000) + .accentColor(0xFF00FF00) + .colorSource("preset") + .themeStyle(ThemeStyle.MONOCHROMATIC) + .colorBoth(false); + + ThemeSettings settings = updater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(2); + assertThat(settings.systemPalette()).isEqualTo(0xFFFF0000); + assertThat(settings.accentColor()).isEqualTo(0xFF00FF00); + assertThat(settings.colorSource()).isEqualTo("preset"); + assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.MONOCHROMATIC); + assertThat(settings.colorBoth()).isEqualTo(false); + } + + /** + * Test that the updater uses null values when no fields are explicitly set. + */ + @Test + public void testUpdater_noFieldsSet() { + ThemeSettingsUpdater updater = ThemeSettings.updater(); + + assertNull(updater.getColorIndex()); + assertNull(updater.getSystemPalette()); + assertNull(updater.getAccentColor()); + assertNull(updater.getColorSource()); + assertNull(updater.getThemeStyle()); + assertNull(updater.getColorBoth()); + } + + /** + * Test that the ThemeSettings object can be correctly parceled and restored. + */ + @Test + public void testParcel_roundTrip() { + ThemeSettingsUpdater updater = ThemeSettings.updater() + .colorIndex(2) + .systemPalette(0xFFFF0000) + .accentColor(0xFF00FF00) + .colorSource("preset") + .themeStyle(ThemeStyle.MONOCHROMATIC) + .colorBoth(false); + + ThemeSettings settings = updater.toThemeSettings(DEFAULTS); + + Parcel parcel = Parcel.obtain(); + settings.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettings fromParcel = ThemeSettings.CREATOR.createFromParcel(parcel); + + assertThat(settings.colorIndex()).isEqualTo(fromParcel.colorIndex()); + assertThat(settings.systemPalette()).isEqualTo(fromParcel.systemPalette()); + assertThat(settings.accentColor()).isEqualTo(fromParcel.accentColor()); + assertThat(settings.colorSource()).isEqualTo(fromParcel.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(fromParcel.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(fromParcel.colorBoth()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java new file mode 100644 index 000000000000..7ce32da7b713 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java @@ -0,0 +1,154 @@ +/* + * 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ThemeSettingsUpdaterTests { + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + private ThemeSettingsUpdater mUpdater; + + @Before + public void setUp() { + mUpdater = ThemeSettings.updater(); + } + + @Test + public void testSetAndGetColorIndex() { + mUpdater.colorIndex(5); + assertThat(mUpdater.getColorIndex()).isEqualTo(5); + } + + @Test + public void testSetAndGetSystemPalette() { + mUpdater.systemPalette(0xFFABCDEF); + assertThat(mUpdater.getSystemPalette()).isEqualTo(0xFFABCDEF); + } + + @Test + public void testSetAndGetAccentColor() { + mUpdater.accentColor(0xFFFEDCBA); + assertThat(mUpdater.getAccentColor()).isEqualTo(0xFFFEDCBA); + } + + @Test + public void testSetAndGetColorSource() { + mUpdater.colorSource("lock_wallpaper"); + assertThat(mUpdater.getColorSource()).isEqualTo("lock_wallpaper"); + } + + @Test + public void testSetAndGetThemeStyle() { + mUpdater.themeStyle(ThemeStyle.EXPRESSIVE); + assertThat(mUpdater.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + } + + @Test + public void testSetAndGetColorBoth() { + mUpdater.colorBoth(false); + assertThat(mUpdater.getColorBoth()).isFalse(); + } + + + @Test + public void testToThemeSettings_allFieldsSet() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF) + .accentColor(0xFFFEDCBA) + .colorSource("lock_wallpaper") + .themeStyle(ThemeStyle.EXPRESSIVE) + .colorBoth(false); + ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(5); + assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF); + assertThat(settings.accentColor()).isEqualTo(0xFFFEDCBA); + assertThat(settings.colorSource()).isEqualTo("lock_wallpaper"); + assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + assertThat(settings.colorBoth()).isFalse(); + } + + @Test + public void testToThemeSettings_someFieldsNotSet_usesDefaults() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF); + + ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(5); + assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF); + assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor()); + assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth()); + } + + @Test + public void testParcel_roundTrip_allFieldsSet() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF) + .accentColor(0xFFFEDCBA) + .colorSource("lock_wallpaper") + .themeStyle(ThemeStyle.EXPRESSIVE) + .colorBoth(false); + + Parcel parcel = Parcel.obtain(); + mUpdater.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel); + + assertThat(fromParcel.getColorIndex()).isEqualTo(5); + assertThat(fromParcel.getSystemPalette()).isEqualTo(0xFFABCDEF); + assertThat(fromParcel.getAccentColor()).isEqualTo(0xFFFEDCBA); + assertThat(fromParcel.getColorSource()).isEqualTo("lock_wallpaper"); + assertThat(fromParcel.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + assertThat(fromParcel.getColorBoth()).isFalse(); + } + + @Test + public void testParcel_roundTrip_noFieldsSet() { + Parcel parcel = Parcel.obtain(); + mUpdater.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel); + + assertThat(fromParcel.getColorIndex()).isNull(); + assertThat(fromParcel.getSystemPalette()).isNull(); + assertThat(fromParcel.getAccentColor()).isNull(); + assertThat(fromParcel.getColorSource()).isNull(); + assertThat(fromParcel.getThemeStyle()).isNull(); + assertThat(fromParcel.getColorBoth()).isNull(); + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 53e82bad818d..8d717bc19e72 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -18,16 +18,12 @@ package com.android.server.policy; import static android.view.KeyEvent.KEYCODE_POWER; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; -import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; import static com.android.server.policy.PhoneWindowManager.POWER_MULTI_PRESS_TIMEOUT_MILLIS; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_TO_SLEEP; -import static org.junit.Assert.assertEquals; - -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.view.Display; @@ -153,143 +149,4 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertNoPowerSleep(); } - - - /** - * Double press of power when the window handles the power key events. The - * system double power gesture launch should not be performed. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerDoublePress_windowHasOverridePermissionAndKeysHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); - - mPhoneWindowManager.assertNoDoublePowerLaunch(); - } - - /** - * Double press of power when the window doesn't handle the power key events. - * The system default gesture launch should be performed and the app should receive both events. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerDoublePress_windowHasOverridePermissionAndKeysUnHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> false); - - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); - mPhoneWindowManager.assertDoublePowerLaunch(); - assertEquals(getDownKeysDispatched(), 2); - assertEquals(getUpKeysDispatched(), 2); - } - - /** - * Triple press of power when the window handles the power key double press gesture. - * The system default gesture launch should not be performed, and the app only receives the - * first two presses. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerTriplePress_windowHasOverridePermissionAndKeysHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); - mPhoneWindowManager.assertNoDoublePowerLaunch(); - assertEquals(getDownKeysDispatched(), 2); - assertEquals(getUpKeysDispatched(), 2); - } - - /** - * Tests a single press, followed by a double press when the window can handle the power key. - * The app should receive all 3 events. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerTriplePressWithDelay_windowHasOverridePermissionAndKeysHandled() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - - sendKey(KEYCODE_POWER); - mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); - sendKey(KEYCODE_POWER); - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertNoDoublePowerLaunch(); - assertEquals(getDownKeysDispatched(), 3); - assertEquals(getUpKeysDispatched(), 3); - } - - /** - * Tests single press when window doesn't handle the power key. Phone should go to sleep. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerSinglePress_windowHasOverridePermissionAndKeyUnhandledByApp() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> false); - mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); - - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.assertPowerSleep(); - } - - /** - * Tests single press when the window handles the power key. Phone should go to sleep after a - * delay of {POWER_MULTI_PRESS_TIMEOUT_MILLIS} - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerSinglePress_windowHasOverridePermissionAndKeyHandledByApp() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); - mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); - - sendKey(KEYCODE_POWER); - - mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); - - mPhoneWindowManager.assertPowerSleep(); - } - - - /** - * Tests 5x press when the window handles the power key. Emergency gesture should still be - * launched. - */ - @Test - @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testPowerFiveTimesPress_windowHasOverridePermissionAndKeyHandledByApp() { - mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); - setDispatchedKeyHandler(keyEvent -> true); - mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); - mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); - - int minEmergencyGestureDurationMillis = mContext.getResources().getInteger( - com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis); - int durationMillis = minEmergencyGestureDurationMillis / 4; - for (int i = 0; i < 5; ++i) { - sendKey(KEYCODE_POWER); - mPhoneWindowManager.moveTimeForward(durationMillis); - } - - mPhoneWindowManager.assertEmergencyLaunch(); - assertEquals(getDownKeysDispatched(), 2); - assertEquals(getUpKeysDispatched(), 2); - } } diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 7059c41898f3..2097d15658a6 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -37,7 +37,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GO_TO_VOICE_ASSIST; @@ -210,8 +209,6 @@ class TestPhoneWindowManager { private int mKeyEventPolicyFlags = FLAG_INTERACTIVE; - private int mProcessPowerKeyDownCount = 0; - private class TestTalkbackShortcutController extends TalkbackShortcutController { TestTalkbackShortcutController(Context context) { super(context); @@ -424,7 +421,7 @@ class TestPhoneWindowManager { doNothing().when(mContext).startActivityAsUser(any(), any()); doNothing().when(mContext).startActivityAsUser(any(), any(), any()); - KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0, 0); + KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0); doReturn(interceptionInfo) .when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any()); @@ -442,9 +439,6 @@ class TestPhoneWindowManager { eq(TEST_BROWSER_ROLE_PACKAGE_NAME)); doReturn(mSmsIntent).when(mPackageManager).getLaunchIntentForPackage( eq(TEST_SMS_ROLE_PACKAGE_NAME)); - mProcessPowerKeyDownCount = 0; - captureProcessPowerKeyDownCount(); - Mockito.reset(mContext); } @@ -715,12 +709,6 @@ class TestPhoneWindowManager { .when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt()); } - void overrideCanWindowOverridePowerKey(boolean granted) { - doReturn(granted) - .when(mButtonOverridePermissionChecker).canWindowOverridePowerKey(any(), anyInt(), - anyInt()); - } - void overrideKeyEventPolicyFlags(int flags) { mKeyEventPolicyFlags = flags; } @@ -800,10 +788,6 @@ class TestPhoneWindowManager { verify(mGestureLauncherService, atMost(4)) .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); - if (overridePowerKeyBehaviorInFocusedWindow()) { - assertTrue(mProcessPowerKeyDownCount >= 2 && mProcessPowerKeyDownCount <= 4); - } - List<Boolean> capturedValues = valueCaptor.getAllValues().stream() .map(mutableBoolean -> mutableBoolean.value) .toList(); @@ -832,10 +816,6 @@ class TestPhoneWindowManager { verify(mGestureLauncherService, atLeast(1)) .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); - if (overridePowerKeyBehaviorInFocusedWindow()) { - assertEquals(mProcessPowerKeyDownCount, 5); - } - List<Boolean> capturedValues = valueCaptor.getAllValues().stream() .map(mutableBoolean -> mutableBoolean.value) .toList(); @@ -1063,12 +1043,4 @@ class TestPhoneWindowManager { verify(mContext, never()).startActivityAsUser(any(), any(), any()); verify(mContext, never()).startActivityAsUser(any(), any()); } - - private void captureProcessPowerKeyDownCount() { - doAnswer((Answer<Void>) invocation -> { - invocation.callRealMethod(); - mProcessPowerKeyDownCount++; - return null; - }).when(mGestureLauncherService).processPowerKeyDown(any()); - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 5427dc22e700..795273d47230 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -29,7 +29,6 @@ import static android.view.Display.FLAG_OWN_FOCUS; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; @@ -49,7 +48,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; @@ -1154,53 +1152,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - @RequiresFlagsEnabled(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testUpdateInputChannel_sanitizeWithoutPermission_ThrowsError() { - final Session session = mock(Session.class); - final int callingUid = Process.FIRST_APPLICATION_UID; - final int callingPid = 1234; - final SurfaceControl surfaceControl = mock(SurfaceControl.class); - final IBinder window = new Binder(); - final InputTransferToken inputTransferToken = mock(InputTransferToken.class); - - - final InputChannel inputChannel = new InputChannel(); - - assertThrows(IllegalArgumentException.class, () -> - mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, - surfaceControl, window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, - 0 /* privateFlags */, - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS, - TYPE_APPLICATION, null /* windowToken */, inputTransferToken, - "TestInputChannel", inputChannel)); - } - - - @Test - @RequiresFlagsEnabled(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) - public void testUpdateInputChannel_sanitizeWithPermission_doesNotThrowError() { - final Session session = mock(Session.class); - final int callingUid = Process.FIRST_APPLICATION_UID; - final int callingPid = 1234; - final SurfaceControl surfaceControl = mock(SurfaceControl.class); - final IBinder window = new Binder(); - final InputTransferToken inputTransferToken = mock(InputTransferToken.class); - - doReturn(PackageManager.PERMISSION_GRANTED).when(mWm.mContext).checkPermission( - android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW, - callingPid, - callingUid); - - final InputChannel inputChannel = new InputChannel(); - - mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl, - window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, - INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS, - TYPE_APPLICATION, null /* windowToken */, inputTransferToken, "TestInputChannel", - inputChannel); - } - - @Test public void testUpdateInputChannel_allowSpyWindowForInputMonitorPermission() { final Session session = mock(Session.class); final int callingUid = Process.SYSTEM_UID; diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index ebe00782319a..68216b2dbd8a 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -912,6 +912,16 @@ public abstract class Connection extends Conferenceable { public static final String EVENT_CALL_HOLD_FAILED = "android.telecom.event.CALL_HOLD_FAILED"; /** + * Connection event used to inform Telecom when a resume operation on a call has failed. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + */ + @FlaggedApi(Flags.FLAG_CALL_SEQUENCING_CALL_RESUME_FAILED) + public static final String EVENT_CALL_RESUME_FAILED = + "android.telecom.event.CALL_RESUME_FAILED"; + + /** * Connection event used to inform Telecom when a switch operation on a call has failed. * <p> * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java index 229c7bfb53e9..9c176cfe45e5 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -60,6 +60,7 @@ import java.util.HashMap; @RunWith(AndroidJUnit4.class) public class IntegrationTests { public static final int WAIT_FOR_TIMEOUT_MS = 5000; + public static final int WAIT_FOR_PENDING_JANKSTATS_MS = 1000; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -116,18 +117,8 @@ public class IntegrationTests { editText.reportAppJankStats(JankUtils.getAppJankStats()); - // reportAppJankStats performs the work on a background thread, check periodically to see - // if the work is complete. - for (int i = 0; i < 10; i++) { - try { - Thread.sleep(100); - if (jankTracker.getPendingJankStats().size() > 0) { - break; - } - } catch (InterruptedException exception) { - //do nothing and continue - } - } + // wait until pending results are available. + JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS); pendingStats = jankTracker.getPendingJankStats(); @@ -222,4 +213,36 @@ public class IntegrationTests { assertTrue(jankTracker.shouldTrack()); } + + /* + When JankTracker is first instantiated it gets passed the apps UID the same UID should be + passed when reporting AppJankStats. To make sure frames and metrics are all associated with + the same app these UIDs need to match. This test confirms that mismatched IDs are not + counted. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void reportJankStats_statNotMerged_onMisMatchedAppIds() { + Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + mDevice.wait(Until.findObject( + By.text(jankTrackerActivity.getString(R.string.continue_test))), + WAIT_FOR_TIMEOUT_MS); + + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); + JankTracker jankTracker = editText.getJankTracker(); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + jankTracker.getPendingJankStats(); + assertEquals(0, pendingStats.size()); + + int mismatchedAppUID = 25; + editText.reportAppJankStats(JankUtils.getAppJankStats(mismatchedAppUID)); + + // wait until pending results should be available. + JankUtils.waitForResults(jankTracker, WAIT_FOR_PENDING_JANKSTATS_MS); + + pendingStats = jankTracker.getPendingJankStats(); + + assertEquals(0, pendingStats.size()); + } } diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java index 9640a84eb9ca..302cad11bbb9 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java @@ -17,17 +17,20 @@ package android.app.jank.tests; import android.app.jank.AppJankStats; +import android.app.jank.JankTracker; import android.app.jank.RelativeFrameTimeHistogram; +import android.os.Process; + public class JankUtils { - private static final int APP_ID = 25; + private static final int APP_ID = Process.myUid(); /** * Returns a mock AppJankStats object to be used in tests. */ - public static AppJankStats getAppJankStats() { + public static AppJankStats getAppJankStats(int appUID) { AppJankStats jankStats = new AppJankStats( - /*App Uid*/APP_ID, + /*App Uid*/appUID, /*Widget Id*/"test widget id", /*navigationComponent*/null, /*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL, @@ -39,6 +42,10 @@ public class JankUtils { return jankStats; } + public static AppJankStats getAppJankStats() { + return getAppJankStats(APP_ID); + } + /** * Returns a mock histogram to be used with an AppJankStats object. */ @@ -50,4 +57,26 @@ public class JankUtils { overrunHistogram.addRelativeFrameTimeMillis(25); return overrunHistogram; } + + /** + * When JankStats are reported they are processed on a background thread. This method checks + * every 100 ms up to the maxWaitTime to see if the pending stat count is greater than zero. + * If the pending stat count is greater than zero it will return or keep trying until + * maxWaitTime has elapsed. + */ + public static void waitForResults(JankTracker jankTracker, int maxWaitTimeMs) { + int currentWaitTimeMs = 0; + int threadSleepTimeMs = 100; + while (currentWaitTimeMs < maxWaitTimeMs) { + try { + Thread.sleep(threadSleepTimeMs); + if (!jankTracker.getPendingJankStats().isEmpty()) { + return; + } + currentWaitTimeMs += threadSleepTimeMs; + } catch (InterruptedException exception) { + // do nothing and continue. + } + } + } } diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 4737d19acde1..1858b1da916b 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -714,18 +714,16 @@ class InputManagerServiceTests { ) } - val info = - KeyInterceptionInfo( - /* type = */ 0, - if (hasPrivateFlag) { - WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS - } else { - 0 - }, - "title", - /* uid = */ 0, - /* inputFeatureFlags = */ 0, - ) + val info = KeyInterceptionInfo( + /* type = */0, + if (hasPrivateFlag) { + WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS + } else { + 0 + }, + "title", + /* uid = */0 + ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } } diff --git a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java index ed256e72b415..34fef25b187c 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java @@ -552,7 +552,7 @@ public class ProcessedPerfettoProtoLogImplTest { } final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); - assertThrows(IllegalStateException.class, reader::readProtoLogTrace); + assertThrows(java.net.ConnectException.class, reader::readProtoLogTrace); } @Test |