diff options
76 files changed, 2269 insertions, 799 deletions
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index c316800108bd..0b9d62983004 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -279,10 +279,6 @@ public class TaskFragmentOrganizer extends WindowOrganizer { * @param state the state to save. */ public void setSavedState(@NonNull Bundle state) { - if (!Flags.aeBackStackRestore()) { - return; - } - if (state.getSize() > 200000) { throw new IllegalArgumentException("Saved state too large, " + state.getSize()); } diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 6f8852daae5f..ac6625b17413 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -91,14 +91,6 @@ flag { flag { namespace: "windowing_sdk" - name: "ae_back_stack_restore" - description: "Allow the ActivityEmbedding back stack to be restored after process restarted" - bug: "289875940" - is_fixed_read_only: true -} - -flag { - namespace: "windowing_sdk" name: "touch_pass_through_opt_in" description: "Requires apps to opt-in to overlay pass through touches and provide APIs to opt-in" bug: "358129114" diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 98d1ef6057fd..7018ebcbe9f4 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -408,7 +408,4 @@ oneway interface IStatusBar * @param displayId the id of the current display. */ void moveFocusedTaskToDesktop(int displayId); - - /** Set whether the display should have a navigation bar. */ - void setHasNavigationBar(int displayId, boolean hasNavigationBar); } diff --git a/core/res/res/drawable/accessibility_autoclick_button_group_rounded_background.xml b/core/res/res/drawable/accessibility_autoclick_button_group_rounded_background.xml new file mode 100644 index 000000000000..87f7cdd61f16 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_button_group_rounded_background.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/materialColorSurfaceContainer" /> + <corners android:radius="30dp" /> +</shape> diff --git a/core/res/res/drawable/accessibility_autoclick_double_click.xml b/core/res/res/drawable/accessibility_autoclick_double_click.xml new file mode 100644 index 000000000000..ea28bc28dbe1 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_double_click.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M9.7 16C8.1 15.9167 6.75 15.3 5.65 14.15C4.55 13 4 11.6167 4 10C4 8.33333 4.58333 6.91667 5.75 5.75C6.91667 4.58333 8.33333 4 10 4C11.6167 4 13 4.55 14.15 5.65C15.3 6.75 15.9167 8.1 16 9.7L13.9 9.075C13.6833 8.175 13.2167 7.44167 12.5 6.875C11.7833 6.29167 10.95 6 10 6C8.9 6 7.95833 6.39167 7.175 7.175C6.39167 7.95833 6 8.9 6 10C6 10.95 6.28333 11.7833 6.85 12.5C7.43333 13.2167 8.175 13.6833 9.075 13.9L9.7 16ZM10.9 19.95C10.75 19.9833 10.6 20 10.45 20C10.3 20 10.15 20 10 20C8.61667 20 7.31667 19.7417 6.1 19.225C4.88333 18.6917 3.825 17.975 2.925 17.075C2.025 16.175 1.30833 15.1167 0.775 13.9C0.258333 12.6833 0.0000000596046 11.3833 0.0000000596046 10C0.0000000596046 8.61667 0.258333 7.31667 0.775 6.1C1.30833 4.88333 2.025 3.825 2.925 2.925C3.825 2.025 4.88333 1.31667 6.1 0.799999C7.31667 0.266666 8.61667 -0.000000476837 10 -0.000000476837C11.3833 -0.000000476837 12.6833 0.266666 13.9 0.799999C15.1167 1.31667 16.175 2.025 17.075 2.925C17.975 3.825 18.6833 4.88333 19.2 6.1C19.7333 7.31667 20 8.61667 20 10C20 10.15 20 10.3 20 10.45C20 10.6 19.9833 10.75 19.95 10.9L18 10.3V10C18 7.76667 17.225 5.875 15.675 4.325C14.125 2.775 12.2333 2 10 2C7.76667 2 5.875 2.775 4.325 4.325C2.775 5.875 2 7.76667 2 10C2 12.2333 2.775 14.125 4.325 15.675C5.875 17.225 7.76667 18 10 18C10.05 18 10.1 18 10.15 18C10.2 18 10.25 18 10.3 18L10.9 19.95ZM18.525 20.5L14.25 16.225L13 20L10 10L20 13L16.225 14.25L20.5 18.525L18.525 20.5Z" + android:fillColor="@color/materialColorPrimary" /> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_drag.xml b/core/res/res/drawable/accessibility_autoclick_drag.xml new file mode 100644 index 000000000000..02d90bc83b5f --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_drag.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M10 20L5.75 15.75L7.175 14.325L9 16.15V11H3.875L5.7 12.8L4.25 14.25L0.0000000596046 10L4.225 5.775L5.65 7.2L3.85 9H9V3.85L7.175 5.675L5.75 4.25L10 -0.000000476837L14.25 4.25L12.825 5.675L11 3.85V9H16.125L14.3 7.2L15.75 5.75L20 10L15.75 14.25L14.325 12.825L16.15 11H11V16.125L12.8 14.3L14.25 15.75L10 20Z" + android:fillColor="@color/materialColorPrimary" /> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_right_click.xml b/core/res/res/drawable/accessibility_autoclick_right_click.xml new file mode 100644 index 000000000000..a5e296614501 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_right_click.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="18" + android:viewportHeight="18"> + <path + android:pathData="M11.3 12L11.925 9.9C12.825 9.68333 13.5583 9.21667 14.125 8.5C14.7083 7.78333 15 6.95 15 6C15 4.9 14.6083 3.95833 13.825 3.175C13.0417 2.39167 12.1 2 11 2C10.05 2 9.21667 2.29167 8.5 2.875C7.78333 3.44167 7.31667 4.175 7.1 5.075L5 5.7C5.08333 4.1 5.7 2.75 6.85 1.65C8 0.549999 9.38333 -0.00000143051 11 -0.00000143051C12.6667 -0.00000143051 14.0833 0.583332 15.25 1.75C16.4167 2.91667 17 4.33333 17 6C17 7.61667 16.45 9 15.35 10.15C14.25 11.3 12.9 11.9167 11.3 12ZM2.475 16.5L0.5 14.525L4.775 10.25L1 9L11 6L8 16L6.75 12.225L2.475 16.5Z" + android:fillColor="@color/materialColorPrimary" /> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll.xml b/core/res/res/drawable/accessibility_autoclick_scroll.xml new file mode 100644 index 000000000000..8b6da25be2ff --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="20dp" + android:viewportWidth="12" + android:viewportHeight="20"> + <path + android:pathData="M10 15L5 20L-0.000000953674 15L1.4 13.6L4 16.175L4 3.825L1.4 6.4L-0.000000953674 5L5 -0.000000476837L10 5L8.6 6.4L6 3.825L6 16.175L8.6 13.6L10 15Z" + android:fillColor="@color/materialColorPrimary" /> +</vector> diff --git a/core/res/res/layout/accessibility_autoclick_type_panel.xml b/core/res/res/layout/accessibility_autoclick_type_panel.xml index 9aa47cc8d68b..cedbdc175488 100644 --- a/core/res/res/layout/accessibility_autoclick_type_panel.xml +++ b/core/res/res/layout/accessibility_autoclick_type_panel.xml @@ -27,26 +27,81 @@ android:padding="16dp"> <LinearLayout + android:id="@+id/accessibility_autoclick_button_group_container" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:orientation="horizontal"> <LinearLayout - android:id="@+id/accessibility_autoclick_left_click_layout" - style="@style/AccessibilityAutoclickPanelButtonLayoutStyle" - android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:background="@drawable/accessibility_autoclick_button_group_rounded_background" + android:orientation="horizontal" + android:padding="3dp"> + + <LinearLayout + android:id="@+id/accessibility_autoclick_drag_layout" + style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"> + + <ImageButton + android:id="@+id/accessibility_autoclick_drag_button" + style="@style/AccessibilityAutoclickPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_drag" + android:src="@drawable/accessibility_autoclick_drag" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/accessibility_autoclick_double_click_layout" + style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"> + + <ImageButton + android:id="@+id/accessibility_autoclick_double_click_button" + style="@style/AccessibilityAutoclickPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_double_click" + android:src="@drawable/accessibility_autoclick_double_click" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/accessibility_autoclick_right_click_layout" + style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"> + + <ImageButton + android:id="@+id/accessibility_autoclick_right_click_button" + style="@style/AccessibilityAutoclickPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_right_click" + android:src="@drawable/accessibility_autoclick_right_click" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/accessibility_autoclick_scroll_layout" + style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"> + + <ImageButton + android:id="@+id/accessibility_autoclick_scroll_button" + style="@style/AccessibilityAutoclickPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll" + android:src="@drawable/accessibility_autoclick_scroll" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/accessibility_autoclick_left_click_layout" + style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"> + + <ImageButton + android:id="@+id/accessibility_autoclick_left_click_button" + style="@style/AccessibilityAutoclickPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_left_click" + android:src="@drawable/accessibility_autoclick_left_click" /> + </LinearLayout> - <ImageButton - android:id="@+id/accessibility_autoclick_left_click_button" - style="@style/AccessibilityAutoclickPanelImageButtonStyle" - android:contentDescription="@string/accessibility_autoclick_left_click" - android:src="@drawable/accessibility_autoclick_left_click" /> </LinearLayout> <View android:layout_width="@dimen/accessibility_autoclick_type_panel_divider_width" android:layout_height="@dimen/accessibility_autoclick_type_panel_divider_height" + android:layout_marginStart="@dimen/accessibility_autoclick_type_panel_button_spacing" android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing" android:background="@color/materialColorSurfaceContainer" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index db806a1d79cb..abbba9d1bffa 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6164,6 +6164,14 @@ <string name="accessibility_autoclick_type_settings_panel_title">Autoclick type settings panel</string> <!-- Label for autoclick left click button [CHAR LIMIT=NONE] --> <string name="accessibility_autoclick_left_click">Left click</string> + <!-- Label for autoclick right click button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_right_click">Right click</string> + <!-- Label for autoclick double click button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_double_click">Double click</string> + <!-- Label for autoclick drag button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_drag">Drag</string> + <!-- Label for autoclick scroll button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll">Scroll</string> <!-- Label for autoclick pause button [CHAR LIMIT=NONE] --> <string name="accessibility_autoclick_pause">Pause</string> <!-- Label for autoclick position button [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8dbed4ca380c..800f98d9a234 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5617,6 +5617,10 @@ <java-symbol type="layout" name="accessibility_autoclick_type_panel" /> <java-symbol type="string" name="accessibility_autoclick_type_settings_panel_title" /> <java-symbol type="string" name="accessibility_autoclick_left_click" /> + <java-symbol type="string" name="accessibility_autoclick_right_click" /> + <java-symbol type="string" name="accessibility_autoclick_double_click" /> + <java-symbol type="string" name="accessibility_autoclick_drag" /> + <java-symbol type="string" name="accessibility_autoclick_scroll" /> <java-symbol type="string" name="accessibility_autoclick_pause" /> <java-symbol type="string" name="accessibility_autoclick_position" /> <java-symbol type="dimen" name="accessibility_autoclick_type_panel_button_spacing" /> @@ -5624,8 +5628,17 @@ <java-symbol type="dimen" name="accessibility_autoclick_type_panel_divider_height" /> <java-symbol type="dimen" name="accessibility_autoclick_type_panel_divider_width" /> <java-symbol type="id" name="accessibility_autoclick_type_panel" /> + <java-symbol type="id" name="accessibility_autoclick_button_group_container" /> <java-symbol type="id" name="accessibility_autoclick_left_click_layout" /> <java-symbol type="id" name="accessibility_autoclick_left_click_button" /> + <java-symbol type="id" name="accessibility_autoclick_right_click_layout" /> + <java-symbol type="id" name="accessibility_autoclick_right_click_button" /> + <java-symbol type="id" name="accessibility_autoclick_double_click_layout" /> + <java-symbol type="id" name="accessibility_autoclick_double_click_button" /> + <java-symbol type="id" name="accessibility_autoclick_drag_layout" /> + <java-symbol type="id" name="accessibility_autoclick_drag_button" /> + <java-symbol type="id" name="accessibility_autoclick_scroll_layout" /> + <java-symbol type="id" name="accessibility_autoclick_scroll_button" /> <java-symbol type="id" name="accessibility_autoclick_pause_layout" /> <java-symbol type="id" name="accessibility_autoclick_pause_button" /> <java-symbol type="id" name="accessibility_autoclick_position_layout" /> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index fcf3a3759f7a..de3146e5bd11 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -85,10 +85,7 @@ class WindowExtensionsImpl implements WindowExtensions { if (Flags.wlinfoOncreate()) { return EXTENSIONS_VERSION_V9; } - if (Flags.aeBackStackRestore()) { - return EXTENSIONS_VERSION_V8; - } - return EXTENSIONS_VERSION_V7; + return EXTENSIONS_VERSION_V8; } private String generateLogMessage() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 1bcb0bb91515..b0fadb06b7e3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -288,7 +288,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mSplitRules.clear(); mSplitRules.addAll(rules); - if (!Flags.aeBackStackRestore() || !mPresenter.isWaitingToRebuildTaskContainers()) { + if (!mPresenter.isWaitingToRebuildTaskContainers()) { return; } @@ -2893,10 +2893,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void setAutoSaveEmbeddingState(boolean saveEmbeddingState) { - if (!Flags.aeBackStackRestore()) { - return; - } - synchronized (mLock) { mPresenter.setAutoSaveEmbeddingState(saveEmbeddingState); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 9a2f32e9ee99..eb59d6efdeff 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -169,12 +169,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mWindowLayoutComponent = windowLayoutComponent; mController = controller; final Bundle outSavedState = new Bundle(); - if (Flags.aeBackStackRestore()) { - outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); - registerOrganizer(false /* isSystemOrganizer */, outSavedState); - } else { - registerOrganizer(); - } + outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); + registerOrganizer(false /* isSystemOrganizer */, outSavedState); mBackupHelper = new BackupHelper(controller, this, outSavedState); if (!SplitController.ENABLE_SHELL_TRANSITIONS) { // TODO(b/207070762): cleanup with legacy app transition diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index b69563b46e06..b1fedce5597e 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -51,7 +51,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" tools:text="Gmail" - android:importantForAccessibility="no" android:textColor="@androidprv:color/materialColorOnSurface" android:textSize="14sp" android:textFontWeight="500" diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellDesktopThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellDesktopThread.java new file mode 100644 index 000000000000..cfa00bbb7649 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellDesktopThread.java @@ -0,0 +1,32 @@ +/* + * 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.wm.shell.shared.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell desktop thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellDesktopThread { +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt index bd129a28f049..da3d44df1180 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt @@ -95,7 +95,7 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi /** Signal the controller that dragging interaction has finished. */ fun onDragEnd() { - getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } + hideDropTarget() dismissZone = null listener?.onRelease(if (onLeft) LEFT else RIGHT) } @@ -139,7 +139,7 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi return rect } - private fun showDropTarget(location: BubbleBarLocation) { + fun showDropTarget(location: BubbleBarLocation) { val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f } if (targetView.alpha > 0) { targetView.animateOut { @@ -152,6 +152,10 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi } } + fun hideDropTarget() { + getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } + } + private fun View.animateIn() { dropTargetAnimator?.cancel() dropTargetAnimator = diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index 84a22b873aaf..481fc7fcb869 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -70,6 +70,7 @@ enum class BubbleBarLocation : Parcelable { UpdateSource.A11Y_ACTION_BAR, UpdateSource.A11Y_ACTION_BUBBLE, UpdateSource.A11Y_ACTION_EXP_VIEW, + UpdateSource.APP_ICON_DRAG ) @Retention(AnnotationRetention.SOURCE) annotation class UpdateSource { @@ -91,6 +92,9 @@ enum class BubbleBarLocation : Parcelable { /** Location changed via a11y action on the expanded view */ const val A11Y_ACTION_EXP_VIEW = 6 + + /** Location changed from dragging the application icon to the bubble bar */ + const val APP_ICON_DRAG = 7 } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 2c81945ffdbe..10efd8e164e4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -832,6 +832,10 @@ public class BubbleController implements ConfigurationChangeListener, case BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW: // TODO(b/349845968): move logging from BubbleBarLayerView to here break; + case BubbleBarLocation.UpdateSource.APP_ICON_DRAG: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_APP_ICON_DROP + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_APP_ICON_DROP); + break; } } @@ -851,24 +855,28 @@ public class BubbleController implements ConfigurationChangeListener, public void onDragItemOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) { if (bubbleBarLocation == null) return; if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { - //TODO(b/388894910) show expanded view drop mBubbleStateListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation); + ensureBubbleViewsAndWindowCreated(); + if (mLayerView != null) { + mLayerView.showBubbleBarExtendedViewDropTarget(bubbleBarLocation); + } } } @Override public void onItemDraggedOutsideBubbleBarDropZone() { if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { - //TODO(b/388894910) hide expanded view drop mBubbleStateListener.onItemDraggedOutsideBubbleBarDropZone(); + hideBubbleBarExpandedViewDropTarget(); } } @Override - public void onItemDroppedOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) { - if (bubbleBarLocation == null) return; + public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent appIntent, + UserHandle userHandle) { if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { - //TODO(b/388894910) handle item drop with expandStackAndSelectBubble() + hideBubbleBarExpandedViewDropTarget(); + expandStackAndSelectBubble(appIntent, userHandle, location); } } @@ -888,6 +896,12 @@ public class BubbleController implements ConfigurationChangeListener, return result; } + private void hideBubbleBarExpandedViewDropTarget() { + if (mLayerView != null) { + mLayerView.hideBubbleBarExpandedViewDropTarget(); + } + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -1512,8 +1526,14 @@ public class BubbleController implements ConfigurationChangeListener, * * @param intent the intent for the bubble. */ - public void expandStackAndSelectBubble(Intent intent, UserHandle user) { + public void expandStackAndSelectBubble(Intent intent, UserHandle user, + @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; + if (bubbleBarLocation != null) { + //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack & + // fix bubble bar flicking + setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + } Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); if (b.isInflated()) { @@ -2746,7 +2766,8 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void showAppBubble(Intent intent, UserHandle user) { - mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, user)); + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, + user, /* bubbleBarLocation = */ null)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java index 347df330c4b3..00b2f15f45eb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java @@ -145,8 +145,14 @@ public class BubbleLogger { @UiEvent(doc = "bubble promoted from overflow back to bubble bar") BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR(1949), + @UiEvent(doc = "application icon is dropped in the BubbleBar left drop zone") + BUBBLE_BAR_MOVED_LEFT_APP_ICON_DROP(2082), + + @UiEvent(doc = "application icon is dropped in the BubbleBar right drop zone") + BUBBLE_BAR_MOVED_RIGHT_APP_ICON_DROP(2083), + @UiEvent(doc = "while bubble bar is expanded, switch to another/existing bubble") - BUBBLE_BAR_BUBBLE_SWITCHED(1977) + BUBBLE_BAR_BUBBLE_SWITCHED(1977), // endregion ; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt index 00eaad675350..afe5c87604d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt @@ -16,7 +16,9 @@ package com.android.wm.shell.bubbles.bar +import android.content.Intent import android.graphics.Rect +import android.os.UserHandle import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** Controller that takes care of the bubble bar drag events. */ @@ -29,7 +31,11 @@ interface BubbleBarDragListener { fun onItemDraggedOutsideBubbleBarDropZone() /** Called when the drop event happens over the bubble bar drop zone. */ - fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation?) + fun onItemDroppedOverBubbleBarDragZone( + location: BubbleBarLocation, + intent: Intent, + userHandle: UserHandle + ) /** * Returns mapping of the bubble bar locations to the corresponding diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index b1035bc177a1..aa42de67152a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -128,6 +128,16 @@ public class BubbleBarLayerView extends FrameLayout setOnClickListener(view -> hideModalOrCollapse()); } + /** Hides the expanded view drop target. */ + public void hideBubbleBarExpandedViewDropTarget() { + mBubbleExpandedViewPinController.hideDropTarget(); + } + + /** Shows the expanded view drop target at the requested {@link BubbleBarLocation location} */ + public void showBubbleBarExtendedViewDropTarget(@NonNull BubbleBarLocation bubbleBarLocation) { + mBubbleExpandedViewPinController.showDropTarget(bubbleBarLocation); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index d7ddbdeaa6da..ee3e39e71558 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -37,6 +37,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.annotations.ExternalMainThread; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellDesktopThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; @@ -193,13 +194,26 @@ public abstract class WMShellConcurrencyModule { } /** + * Provides a Shell desktop thread Executor + */ + @WMSingleton + @Provides + @ShellDesktopThread + public static ShellExecutor provideDesktopModeMiscExecutor() { + HandlerThread shellDesktopThread = new HandlerThread("wmshell.desktop", + THREAD_PRIORITY_TOP_APP_BOOST); + shellDesktopThread.start(); + return new HandlerExecutor(shellDesktopThread.getThreadHandler()); + } + + /** * Provides a Shell background thread Handler for low priority background tasks. */ @WMSingleton @Provides @ShellBackgroundThread public static Handler provideSharedBackgroundHandler() { - HandlerThread shellBackgroundThread = new HandlerThread("wmshell.background", + final HandlerThread shellBackgroundThread = new HandlerThread("wmshell.background", THREAD_PRIORITY_BACKGROUND); shellBackgroundThread.start(); return shellBackgroundThread.getThreadHandler(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 6ab103e3bd89..d26789d2781a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -53,7 +53,6 @@ import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.back.BackAnimationController; -import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; @@ -61,6 +60,7 @@ import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleResizabilityChecker; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.storage.BubblePersistentRepository; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; @@ -170,18 +170,19 @@ import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromo import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController; import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - import dagger.Binds; import dagger.Lazy; import dagger.Module; import dagger.Provides; + import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.ExperimentalCoroutinesApi; import kotlinx.coroutines.MainCoroutineDispatcher; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + /** * Provides dependencies from {@link com.android.wm.shell}, these dependencies are only accessible * from components within the WM subcomponent (can be explicitly exposed to the SysUIComponent, see diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index f0e0295336a7..ea0f09af2d3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -38,6 +38,7 @@ import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.StatusBarManager; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; @@ -46,6 +47,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; +import android.os.UserHandle; import android.view.DragEvent; import android.view.SurfaceControl; import android.view.View; @@ -600,6 +602,12 @@ public class DragLayout extends LinearLayout @Nullable private BubbleBarLocation getBubbleBarLocation(int x, int y) { + Intent appData = mSession.appData; + if (appData == null || appData.getExtra(Intent.EXTRA_INTENT) == null + || appData.getExtra(Intent.EXTRA_USER) == null) { + // there is no app data, so drop event over the bubble bar can not be handled + return null; + } for (BubbleBarLocation location : mBubbleBarLocations.keySet()) { if (mBubbleBarLocations.get(location).contains(x, y)) { return location; @@ -649,11 +657,17 @@ public class DragLayout extends LinearLayout @Nullable WindowContainerToken hideTaskToken, Runnable dropCompleteCallback) { final boolean handledDrop = mCurrentTarget != null || mCurrentBubbleBarTarget != null; mHasDropped = true; - - // Process the drop - mPolicy.onDropped(mCurrentTarget, hideTaskToken); - //TODO(b/388894910) add info about the application - mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget); + Intent appData = mSession.appData; + + // Process the drop exclusive by DropTarget OR by the BubbleBar + if (mCurrentTarget != null) { + mPolicy.onDropped(mCurrentTarget, hideTaskToken); + } else if (appData != null && mCurrentBubbleBarTarget != null) { + Intent appIntent = (Intent) appData.getExtra(Intent.EXTRA_INTENT); + UserHandle user = (UserHandle) appData.getExtra(Intent.EXTRA_USER); + mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget, + appIntent, user); + } // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 53d3b77f1ba2..bb2a53bc04d6 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -779,6 +779,7 @@ cc_test { "tests/unit/CommonPoolTests.cpp", "tests/unit/DamageAccumulatorTests.cpp", "tests/unit/DeferredLayerUpdaterTests.cpp", + "tests/unit/DrawTextFunctorTest.cpp", "tests/unit/EglManagerTests.cpp", "tests/unit/FatVectorTests.cpp", "tests/unit/GraphicsStatsServiceTests.cpp", diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 008b693edf02..b782837bb4ee 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -76,6 +76,41 @@ static void simplifyPaint(int color, Paint* paint) { paint->setBlendMode(SkBlendMode::kSrcOver); } +namespace { + +static bool shouldDarkenTextForHighContrast(const uirenderer::Lab& lab) { + // LINT.IfChange(hct_darken) + return lab.L <= 50; + // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) +} + +} // namespace + +static void adjustHighContrastInnerTextColor(uirenderer::Lab* lab) { + bool darken = shouldDarkenTextForHighContrast(*lab); + bool isGrayscale = abs(lab->a) < 10 && abs(lab->b) < 10; + if (isGrayscale) { + // For near-grayscale text we first remove all color. + lab->a = lab->b = 0; + if (lab->L > 40 && lab->L < 60) { + // Text near "middle gray" is pushed to a more contrasty gray. + lab->L = darken ? 20 : 80; + } else { + // Other grayscale text is pushed completely white or black. + lab->L = darken ? 0 : 100; + } + } else { + // For color text we ensure the text is bright enough (for light text) + // or dark enough (for dark text) to stand out against the background, + // without touching the A and B components so we retain color. + if (darken && lab->L > 20.f) { + lab->L = 20.0f; + } else if (!darken && lab->L < 90.f) { + lab->L = 90.0f; + } + } +} + class DrawTextFunctor { public: /** @@ -114,10 +149,8 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - // LINT.IfChange(hct_darken) uirenderer::Lab lab = uirenderer::sRGBToLab(color); - bool darken = lab.L <= 50; - // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) + bool darken = shouldDarkenTextForHighContrast(lab); // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; @@ -130,20 +163,7 @@ public: gDrawTextBlobMode = DrawTextBlobMode::HctInner; Paint innerPaint(paint); if (flags::high_contrast_text_inner_text_color()) { - // Preserve some color information while still ensuring sufficient contrast. - // Thus we increase the lightness to make the color stand out against a black - // background, and vice-versa. For grayscale, we retain some gray to indicate - // states like disabled or to distinguish links. - bool isGrayscale = abs(lab.a) < 1 && abs(lab.b) < 1; - if (isGrayscale) { - if (darken) { - lab.L = lab.L < 40 ? 0 : 20; - } else { - lab.L = lab.L > 60 ? 100 : 80; - } - } else { - lab.L = darken ? 20 : 90; - } + adjustHighContrastInnerTextColor(&lab); simplifyPaint(uirenderer::LabToSRGB(lab, SK_AlphaOPAQUE), &innerPaint); } else { simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint); diff --git a/libs/hwui/tests/unit/DrawTextFunctorTest.cpp b/libs/hwui/tests/unit/DrawTextFunctorTest.cpp new file mode 100644 index 000000000000..c5361a0833c4 --- /dev/null +++ b/libs/hwui/tests/unit/DrawTextFunctorTest.cpp @@ -0,0 +1,88 @@ +/* + * 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. + */ + +#include <gtest/gtest.h> + +#include "hwui/DrawTextFunctor.h" + +using namespace android; + +namespace { + +void testHighContrastInnerTextColor(float originalL, float originalA, float originalB, + float expectedL, float expectedA, float expectedB) { + uirenderer::Lab color = {originalL, originalA, originalB}; + adjustHighContrastInnerTextColor(&color); + EXPECT_FLOAT_EQ(color.L, expectedL); + EXPECT_FLOAT_EQ(color.a, expectedA); + EXPECT_FLOAT_EQ(color.b, expectedB); +} + +TEST(DrawTextFunctorTest, BlackUnaffected) { + testHighContrastInnerTextColor(0, 0, 0, 0, 0, 0); +} + +TEST(DrawTextFunctorTest, WhiteUnaffected) { + testHighContrastInnerTextColor(100, 0, 0, 100, 0, 0); +} + +TEST(DrawTextFunctorTest, DarkGrayPushedToWhite) { + testHighContrastInnerTextColor(10, 0, 0, 0, 0, 0); + testHighContrastInnerTextColor(20, 0, 0, 0, 0, 0); +} + +TEST(DrawTextFunctorTest, LightGrayPushedToWhite) { + testHighContrastInnerTextColor(80, 0, 0, 100, 0, 0); + testHighContrastInnerTextColor(90, 0, 0, 100, 0, 0); +} + +TEST(DrawTextFunctorTest, MiddleDarkGrayPushedToDarkGray) { + testHighContrastInnerTextColor(41, 0, 0, 20, 0, 0); + testHighContrastInnerTextColor(49, 0, 0, 20, 0, 0); +} + +TEST(DrawTextFunctorTest, MiddleLightGrayPushedToLightGray) { + testHighContrastInnerTextColor(51, 0, 0, 80, 0, 0); + testHighContrastInnerTextColor(59, 0, 0, 80, 0, 0); +} + +TEST(DrawTextFunctorTest, PaleColorTreatedAsGrayscaleAndPushedToWhite) { + testHighContrastInnerTextColor(75, 5, -5, 100, 0, 0); + testHighContrastInnerTextColor(85, -6, 8, 100, 0, 0); +} + +TEST(DrawTextFunctorTest, PaleColorTreatedAsGrayscaleAndPushedToBlack) { + testHighContrastInnerTextColor(25, 5, -5, 0, 0, 0); + testHighContrastInnerTextColor(35, -6, 8, 0, 0, 0); +} + +TEST(DrawTextFunctorTest, ColorfulColorIsLightened) { + testHighContrastInnerTextColor(70, 100, -100, 90, 100, -100); +} + +TEST(DrawTextFunctorTest, ColorfulLightColorIsUntouched) { + testHighContrastInnerTextColor(95, 100, -100, 95, 100, -100); +} + +TEST(DrawTextFunctorTest, ColorfulColorIsDarkened) { + testHighContrastInnerTextColor(30, 100, -100, 20, 100, -100); +} + +TEST(DrawTextFunctorTest, ColorfulDarkColorIsUntouched) { + testHighContrastInnerTextColor(5, 100, -100, 5, 100, -100); +} + +} // namespace 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 8054bd113771..a3fde5a06573 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 @@ -27,6 +27,7 @@ import android.app.Notification.ProgressStyle.Segment import android.app.PendingIntent import android.app.Person import android.content.Intent +import android.graphics.drawable.Icon import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -38,6 +39,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style +import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -49,6 +51,8 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { private val kosmos = testKosmos() private val underTest = kosmos.promotedNotificationContentExtractor + private val rowImageInflater = RowImageInflater.newInstance(previousIndex = null) + private val imageModelProvider by lazy { rowImageInflater.useForContentModel() } @Test @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) @@ -294,14 +298,18 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? { val recoveredBuilder = Notification.Builder(context, entry.sbn.notification) - return underTest.extractContent(entry, recoveredBuilder) + return underTest.extractContent(entry, recoveredBuilder, imageModelProvider) } private fun createEntry( promoted: Boolean = true, builderBlock: Notification.Builder.() -> Unit = {}, ): NotificationEntry { - val notif = Notification.Builder(context, "channel").also(builderBlock).build() + val notif = + Notification.Builder(context, "channel") + .setSmallIcon(Icon.createWithContentUri("content://foo/bar")) + .also(builderBlock) + .build() if (promoted) { notif.flags = FLAG_PROMOTED_ONGOING } 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 680b1bee72b2..c77b09aac2b9 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 @@ -203,6 +203,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { val result = NotificationRowContentBinderImpl.InflationProgress( packageContext = mContext, + rowImageInflater = RowImageInflater.newInstance(null), remoteViews = NewRemoteViews(), contentModel = NotificationContentModel(headsUpStatusBarModel), promotedContent = null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt new file mode 100644 index 000000000000..86689cb88569 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020 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.row + +import android.graphics.drawable.Icon +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.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.row.shared.ImageModel +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.SmallSquare +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(PromotedNotificationUiAod.FLAG_NAME) +class RowImageInflaterTest : SysuiTestCase() { + private lateinit var rowImageInflater: RowImageInflater + + private val resIcon1 = Icon.createWithResource(context, android.R.drawable.ic_info) + private val resIcon2 = Icon.createWithResource(context, android.R.drawable.ic_delete) + private val badUriIcon = Icon.createWithContentUri("content://com.test/does_not_exist") + + @Before + fun setUp() { + rowImageInflater = RowImageInflater.newInstance(null) + } + + @Test + fun getNewImageIndex_returnsNullWhenUnused() { + assertThat(rowImageInflater.getNewImageIndex()).isNull() + } + + @Test + fun getNewImageIndex_returnsEmptyIndexWhenZeroImagesLoaded() { + assertThat(getImageModelsForIcons()).isEmpty() + val result = rowImageInflater.getNewImageIndex() + assertThat(result).isNotNull() + assertThat(result?.contentsForTesting).isEmpty() + } + + @Test + fun getNewImageIndex_returnsSingleImageWhenOneImageLoaded() { + assertThat(getImageModelsForIcons(resIcon1)).hasSize(1) + val result = rowImageInflater.getNewImageIndex() + assertThat(result).isNotNull() + assertThat(result?.contentsForTesting).hasSize(1) + } + + @Test + fun exampleFirstGeneration() { + // GIVEN various models are required + val providedModels = getImageModelsForIcons(resIcon1, badUriIcon, resIcon1, resIcon2) + + // VERIFY that we get 4 models, and the two with the same value are shared + assertThat(providedModels).hasSize(4) + assertThat(providedModels[0]).isNotSameInstanceAs(providedModels[1]) + assertThat(providedModels[0]).isSameInstanceAs(providedModels[2]) + assertThat(providedModels[0]).isNotSameInstanceAs(providedModels[3]) + + // THEN load images + rowImageInflater.loadImagesSynchronously(context) + + // VERIFY that the valid drawables are loaded + assertThat(providedModels[0].drawable).isNotNull() + assertThat(providedModels[1].drawable).isNull() + assertThat(providedModels[2].drawable).isNotNull() + assertThat(providedModels[3].drawable).isNotNull() + + // VERIFY the returned index has all 3 entries, 2 of which have drawables + val indexGen1 = rowImageInflater.getNewImageIndex() + assertThat(indexGen1).isNotNull() + assertThat(indexGen1?.contentsForTesting).hasSize(3) + assertThat(indexGen1?.contentsForTesting?.mapNotNull { it.drawable }).hasSize(2) + } + + @Test + fun exampleSecondGeneration_whichLoadsNothing() { + exampleFirstGeneration() + + // THEN start a new generation of the inflation + rowImageInflater = RowImageInflater.newInstance(rowImageInflater.getNewImageIndex()) + + getNewImageIndex_returnsEmptyIndexWhenZeroImagesLoaded() + } + + @Test + fun exampleSecondGeneration_whichLoadsOneImage() { + exampleFirstGeneration() + + // THEN start a new generation of the inflation + rowImageInflater = RowImageInflater.newInstance(rowImageInflater.getNewImageIndex()) + + getNewImageIndex_returnsSingleImageWhenOneImageLoaded() + } + + private fun getImageModelsForIcons(vararg icons: Icon): List<ImageModel> { + val provider = rowImageInflater.useForContentModel() + return icons.map { icon -> + requireNotNull(provider.getImageModel(icon, SmallSquare)) { "null model for $icon" } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 7659fb42fba7..01ba4df3a314 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -6,7 +6,6 @@ import android.platform.test.flag.junit.FlagsParameterization import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation.getContentAlpha import com.android.systemui.dump.DumpManager @@ -479,11 +478,7 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) val marginBottom = - context.resources.getDimensionPixelSize( - if (Flags.notificationsRedesignFooterView()) - R.dimen.notification_2025_panel_margin_bottom - else R.dimen.notification_panel_margin_bottom - ) + context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index af00e2a1a639..21297e3e64b7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -17,12 +17,9 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState -import com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository import com.android.systemui.common.shared.model.NotificationContainerBounds @@ -298,7 +295,6 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test - @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) fun validateMarginBottom() = testScope.runTest { overrideResource(R.dimen.notification_panel_margin_bottom, 50) @@ -311,19 +307,6 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test - @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) - fun validateMarginBottom_footerRedesign() = - testScope.runTest { - overrideResource(R.dimen.notification_2025_panel_margin_bottom, 50) - - val dimens by collectLastValue(underTest.configurationBasedDimensions) - - configurationRepository.onAnyConfigurationChange() - - assertThat(dimens!!.marginBottom).isEqualTo(50) - } - - @Test @DisableSceneContainer fun validateMarginTopWithLargeScreenHeader_usesHelper() = testScope.runTest { diff --git a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml index 5954de4012c8..65cf81ea416b 100644 --- a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml +++ b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml @@ -16,7 +16,6 @@ --> <!-- This XML is served to be overridden by other OEMs/device types. --> -<!-- Note: The margins may be overridden in code, see NotificationStackScrollLayout#getBottomMargin --> <com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:systemui="http://schemas.android.com/apk/res-auto" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c9c4f8cc56e0..fcd3a51d5bb3 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -574,8 +574,6 @@ <dimen name="notification_panel_margin_bottom">32dp</dimen> - <dimen name="notification_2025_panel_margin_bottom">64dp</dimen> - <!-- The bottom padding of the panel that holds the list of notifications. --> <dimen name="notification_panel_padding_bottom">0dp</dimen> diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java index a836dcbf848b..777a50f0936a 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java @@ -16,8 +16,6 @@ package com.android.keyguard; -import static com.android.systemui.Flags.gsfBouncer; - import android.content.Context; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -30,6 +28,7 @@ import android.widget.Button; import com.android.internal.util.EmergencyAffordanceManager; import com.android.systemui.Flags; +import com.android.systemui.FontStyles; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; /** @@ -75,10 +74,8 @@ public class EmergencyButton extends Button { return false; }); } - if (gsfBouncer() || Flags.bouncerUiRevamp2()) { - setTypeface(Typeface.create("gsf-title-medium", Typeface.NORMAL)); - } if (Flags.bouncerUiRevamp2()) { + setTypeface(Typeface.create(FontStyles.GSF_TITLE_MEDIUM, Typeface.NORMAL)); Drawable background = getBackground(); int bgColor = mContext.getColor(KeyguardBouncerConstants.Color.actionButtonBg); if (background != null) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 63d4fe3f1b01..335a910eb106 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -32,7 +32,7 @@ import static androidx.constraintlayout.widget.ConstraintSet.START; import static androidx.constraintlayout.widget.ConstraintSet.TOP; import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; -import static com.android.systemui.Flags.gsfBouncer; +import static com.android.systemui.Flags.bouncerUiRevamp2; import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; import static java.lang.Integer.max; @@ -99,6 +99,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.settingslib.Utils; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.systemui.Flags; +import com.android.systemui.FontStyles; import com.android.systemui.Gefingerpoken; import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.plugins.FalsingManager; @@ -1348,8 +1349,9 @@ public class KeyguardSecurityContainer extends ConstraintLayout { true); mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher); mUserSwitcher = mView.findViewById(R.id.user_switcher_header); - if (gsfBouncer()) { - mUserSwitcher.setTypeface(Typeface.create("gsf-label-medium", Typeface.NORMAL)); + if (bouncerUiRevamp2()) { + mUserSwitcher.setTypeface( + Typeface.create(FontStyles.GSF_LABEL_MEDIUM, Typeface.NORMAL)); } } diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java index 3ceba5a97b17..b152ff348e22 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java @@ -15,7 +15,7 @@ */ package com.android.keyguard; -import static com.android.systemui.Flags.gsfBouncer; +import static com.android.systemui.Flags.bouncerUiRevamp2; import android.content.Context; import android.content.res.Configuration; @@ -37,7 +37,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import com.android.settingslib.Utils; -import com.android.systemui.Flags; +import com.android.systemui.FontStyles; import com.android.systemui.bouncer.shared.constants.PinBouncerConstants.Color; import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer; import com.android.systemui.res.R; @@ -145,6 +145,11 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { } else { mAnimator = null; } + + if (bouncerUiRevamp2()) { + mDigitText.setTypeface( + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)); + } } @Override @@ -160,9 +165,6 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { int klondikeColor = Utils.getColorAttr(getContext(), android.R.attr.textColorSecondary) .getDefaultColor(); mDigitText.setTextColor(textColor); - if (gsfBouncer() || Flags.bouncerUiRevamp2()) { - mDigitText.setTypeface(Typeface.create("gsf-label-large-emphasized", Typeface.NORMAL)); - } mKlondikeText.setTextColor(klondikeColor); if (mAnimator != null) mAnimator.reloadColors(getContext()); diff --git a/packages/SystemUI/src/com/android/systemui/FontStyles.kt b/packages/SystemUI/src/com/android/systemui/FontStyles.kt index d8cd6c87a1ac..5315dcbc4790 100644 --- a/packages/SystemUI/src/com/android/systemui/FontStyles.kt +++ b/packages/SystemUI/src/com/android/systemui/FontStyles.kt @@ -18,11 +18,45 @@ package com.android.systemui /** String tokens for the different GSF font families. */ object FontStyles { + // baseline + const val GSF_DISPLAY_LARGE = "variable-display-large" + const val GSF_DISPLAY_MEDIUM = "variable-display-medium" + const val GSF_DISPLAY_SMALL = "variable-display-small" - const val GSF_LABEL_MEDIUM = "gsf-label-medium" - const val GSF_LABEL_LARGE = "gsf-label-large" + const val GSF_HEADLINE_LARGE = "variable-headline-large" + const val GSF_HEADLINE_MEDIUM = "variable-headline-medium" + const val GSF_HEADLINE_SMALL = "variable-headline-small" - const val GSF_BODY_MEDIUM = "gsf-body-medium" + const val GSF_TITLE_LARGE = "variable-title-large" + const val GSF_TITLE_MEDIUM = "variable-title-medium" + const val GSF_TITLE_SMALL = "variable-title-small" - const val GSF_TITLE_SMALL_EMPHASIZED = "gsf-title-small-emphasized" + const val GSF_LABEL_LARGE = "variable-label-large" + const val GSF_LABEL_MEDIUM = "variable-label-medium" + const val GSF_LABEL_SMALL = "variable-label-small" + + const val GSF_BODY_LARGE = "variable-body-large" + const val GSF_BODY_MEDIUM = "variable-body-medium" + const val GSF_BODY_SMALL = "variable-body-small" + + // emphasized + const val GSF_DISPLAY_LARGE_EMPHASIZED = "variable-display-large-emphasized" + const val GSF_DISPLAY_MEDIUM_EMPHASIZED = "variable-display-medium-emphasized" + const val GSF_DISPLAY_SMALL_EMPHASIZED = "variable-display-small-emphasized" + + const val GSF_HEADLINE_LARGE_EMPHASIZED = "variable-headline-large-emphasized" + const val GSF_HEADLINE_MEDIUM_EMPHASIZED = "variable-headline-medium-emphasized" + const val GSF_HEADLINE_SMALL_EMPHASIZED = "variable-headline-small-emphasized" + + const val GSF_TITLE_LARGE_EMPHASIZED = "variable-title-large-emphasized" + const val GSF_TITLE_MEDIUM_EMPHASIZED = "variable-title-medium-emphasized" + const val GSF_TITLE_SMALL_EMPHASIZED = "variable-title-small-emphasized" + + const val GSF_LABEL_LARGE_EMPHASIZED = "variable-label-large-emphasized" + const val GSF_LABEL_MEDIUM_EMPHASIZED = "variable-label-medium-emphasized" + const val GSF_LABEL_SMALL_EMPHASIZED = "variable-label-small-emphasized" + + const val GSF_BODY_LARGE_EMPHASIZED = "variable-body-large-emphasized" + const val GSF_BODY_MEDIUM_EMPHASIZED = "variable-body-medium-emphasized" + const val GSF_BODY_SMALL_EMPHASIZED = "variable-body-small-emphasized" } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt index b53a8a1fe671..b463dd447f2b 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt @@ -24,6 +24,7 @@ import com.android.keyguard.BouncerKeyguardMessageArea import com.android.keyguard.KeyguardMessageArea import com.android.keyguard.KeyguardMessageAreaController import com.android.systemui.Flags +import com.android.systemui.FontStyles import com.android.systemui.res.R class BouncerMessageView : LinearLayout { @@ -45,12 +46,12 @@ class BouncerMessageView : LinearLayout { primaryMessageView = findViewById(R.id.bouncer_primary_message_area) secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area) - if (Flags.gsfBouncer() || Flags.bouncerUiRevamp2()) { + if (Flags.bouncerUiRevamp2()) { primaryMessageView?.apply { - typeface = Typeface.create("gsf-title-large-emphasized", Typeface.NORMAL) + typeface = Typeface.create(FontStyles.GSF_TITLE_LARGE_EMPHASIZED, Typeface.NORMAL) } secondaryMessageView?.apply { - typeface = Typeface.create("gsf-title-medium-emphasized", Typeface.NORMAL) + typeface = Typeface.create(FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED, Typeface.NORMAL) } } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java index babb64050ed5..5fa0095d2329 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java @@ -318,13 +318,6 @@ public class NavigationBarControllerImpl implements navBarView.showPinningEscapeToast(); } } - - @Override - public void setHasNavigationBar(int displayId, boolean hasNavigationBar) { - if (enableDisplayContentModeManagement()) { - mHasNavBar.put(displayId, hasNavigationBar); - } - } }; /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 5b44c082bd72..cf310dd32613 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -39,7 +39,6 @@ import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.system.QuickStepContract -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.LargeScreenUtils @@ -156,7 +155,8 @@ constructor( val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled splitShadeEnabled = newSplitShadeEnabled largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources) - notificationsBottomMargin = NotificationStackScrollLayout.getBottomMargin(context) + notificationsBottomMargin = + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight() shadeHeaderHeight = calculateShadeHeaderHeight() panelMarginHorizontal = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 1720898229a5..97de61969ffb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -184,7 +184,6 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_SET_SPLITSCREEN_FOCUS = 81 << MSG_SHIFT; private static final int MSG_TOGGLE_QUICK_SETTINGS_PANEL = 82 << MSG_SHIFT; private static final int MSG_WALLET_ACTION_LAUNCH_GESTURE = 83 << MSG_SHIFT; - private static final int MSG_SET_HAS_NAVIGATION_BAR = 84 << MSG_SHIFT; private static final int MSG_DISPLAY_REMOVE_SYSTEM_DECORATIONS = 85 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -588,12 +587,6 @@ public class CommandQueue extends IStatusBar.Stub implements * @see IStatusBar#moveFocusedTaskToDesktop(int) */ default void moveFocusedTaskToDesktop(int displayId) {} - - /** - * @see IStatusBar#setHasNavigationBar(int, boolean) - */ - default void setHasNavigationBar(int displayId, boolean hasNavigationBar) { - } } @VisibleForTesting @@ -1532,14 +1525,6 @@ public class CommandQueue extends IStatusBar.Stub implements mHandler.obtainMessage(MSG_ENTER_DESKTOP, args).sendToTarget(); } - @Override - public void setHasNavigationBar(int displayId, boolean hasNavigationBar) { - synchronized (mLock) { - mHandler.obtainMessage(MSG_SET_HAS_NAVIGATION_BAR, displayId, - hasNavigationBar ? 1 : 0).sendToTarget(); - } - } - private final class H extends Handler { private H(Looper l) { @@ -2072,11 +2057,6 @@ public class CommandQueue extends IStatusBar.Stub implements } break; } - case MSG_SET_HAS_NAVIGATION_BAR: - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).setHasNavigationBar(msg.arg1, msg.arg2 != 0); - } - break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 3dbf0698dce9..91653d314681 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -322,7 +322,7 @@ public interface NotificationsModule { if (PromotedNotificationContentModel.featureFlagEnabled()) { return implProvider.get(); } else { - return (entry, recoveredBuilder) -> null; + return (entry, recoveredBuilder, imageModelProvider) -> null; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index ec15ae25ebfd..5ddb4ea4b94a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -55,12 +55,15 @@ import com.android.internal.widget.NotificationProgressModel import com.android.internal.widget.NotificationRowIconView import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R as systemuiR +import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.Background import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.PrimaryText import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.SecondaryText import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel +import com.android.systemui.statusbar.notification.row.shared.ImageModel +import com.android.systemui.statusbar.notification.row.shared.isNullOrEmpty @Composable fun AODPromotedNotification(viewModelFactory: AODPromotedNotificationViewModel.Factory) { @@ -171,6 +174,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { root.findViewById(R.id.header_text_secondary_divider) private val icon: NotificationRowIconView? = root.findViewById(R.id.icon) private val leftIcon: ImageView? = root.findViewById(R.id.left_icon) + private val rightIcon: ImageView? = root.findViewById(R.id.right_icon) private val notificationProgressEndIcon: CachingIconView? = root.findViewById(R.id.notification_progress_end_icon) private val notificationProgressStartIcon: CachingIconView? = @@ -224,10 +228,16 @@ private class AODPromotedNotificationViewUpdater(root: View) { textView: ImageFloatingTextView? = null, showOldProgress: Boolean = true, ) { + // Icon binding must be called in this order + updateImageView(icon, content.smallIcon) + icon?.setBackgroundColor(Background.colorInt) + icon?.originalIconColor = PrimaryText.colorInt + updateHeader(content, hideTitle = true) updateTitle(title, content) updateText(textView ?: text, content) + updateImageView(rightIcon, content.skeletonLargeIcon) if (showOldProgress) { updateOldProgressBar(content) @@ -327,6 +337,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { updateTimeAndChronometer(content) updateConversationHeaderDividers(content, hideTitle = true) + updateImageView(verificationIcon, content.verificationIcon) updateTextView(verificationText, content.verificationText) } @@ -337,7 +348,8 @@ private class AODPromotedNotificationViewUpdater(root: View) { val hasTitle = content.title != null && !hideTitle val hasAppName = content.appName != null val hasTimeOrChronometer = content.time != null - val hasVerification = content.verificationIcon != null || content.verificationText != null + val hasVerification = + !content.verificationIcon.isNullOrEmpty() || !content.verificationText.isNullOrEmpty() val hasTextBeforeAppName = hasTitle val hasTextBeforeTime = hasTitle || hasAppName @@ -415,15 +427,18 @@ private class AODPromotedNotificationViewUpdater(root: View) { text: CharSequence?, color: AodPromotedNotificationColor = SecondaryText, ) { + if (view == null) return setTextViewColor(view, color) - if (text != null && text.isNotEmpty()) { - view?.text = text.toSkeleton() - view?.visibility = VISIBLE - } else { - view?.text = "" - view?.visibility = GONE - } + view.text = text?.toSkeleton() ?: "" + view.isVisible = !text.isNullOrEmpty() + } + + private fun updateImageView(view: ImageView?, model: ImageModel?) { + if (view == null) return + val drawable = model?.drawable + view.setImageDrawable(drawable) + view.isVisible = drawable != null } private fun setTextViewColor(view: TextView?, color: AodPromotedNotificationColor) { @@ -464,7 +479,7 @@ private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.Progress } private enum class AodPromotedNotificationColor(colorUInt: UInt) { - Background(0x00000000u), + Background(0xFF000000u), PrimaryText(0xFFFFFFFFu), SecondaryText(0xFFCCCCCCu); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index 035edd9711bd..cd7872291801 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -27,8 +27,11 @@ import android.app.Notification.EXTRA_PROGRESS_MAX import android.app.Notification.EXTRA_SUB_TEXT import android.app.Notification.EXTRA_TEXT import android.app.Notification.EXTRA_TITLE +import android.app.Notification.EXTRA_VERIFICATION_ICON +import android.app.Notification.EXTRA_VERIFICATION_TEXT import android.app.Notification.ProgressStyle import android.content.Context +import android.graphics.drawable.Icon import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.shade.ShadeDisplayAware @@ -40,12 +43,18 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When +import com.android.systemui.statusbar.notification.row.shared.ImageModel +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.MediumSquare +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.SmallSquare +import com.android.systemui.statusbar.notification.row.shared.SkeletonImageTransform import javax.inject.Inject interface PromotedNotificationContentExtractor { fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, + imageModelProvider: ImageModelProvider, ): PromotedNotificationContentModel? } @@ -54,11 +63,13 @@ class PromotedNotificationContentExtractorImpl @Inject constructor( @ShadeDisplayAware private val context: Context, + private val skeletonImageTransform: SkeletonImageTransform, private val logger: PromotedNotificationLogger, ) : PromotedNotificationContentExtractor { override fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, + imageModelProvider: ImageModelProvider, ): PromotedNotificationContentModel? { if (!PromotedNotificationContentModel.featureFlagEnabled()) { logger.logExtractionSkipped(entry, "feature flags disabled") @@ -84,7 +95,7 @@ constructor( contentBuilder.wasPromotedAutomatically = notification.extras.getBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) - contentBuilder.skeletonSmallIcon = entry.icons.aodIcon?.sourceIcon + contentBuilder.smallIcon = notification.smallIconModel(imageModelProvider) contentBuilder.appName = notification.loadHeaderAppName(context) contentBuilder.subText = notification.subText() contentBuilder.time = notification.extractWhen() @@ -93,7 +104,7 @@ constructor( contentBuilder.profileBadgeResId = null // TODO contentBuilder.title = notification.title() contentBuilder.text = notification.text() - contentBuilder.skeletonLargeIcon = null // TODO + contentBuilder.skeletonLargeIcon = notification.skeletonLargeIcon(imageModelProvider) contentBuilder.oldProgress = notification.oldProgress() val colorsFromNotif = recoveredBuilder.getColors(/* header= */ false) @@ -103,113 +114,144 @@ constructor( primaryTextColor = colorsFromNotif.primaryTextColor, ) - recoveredBuilder.extractStyleContent(contentBuilder) + recoveredBuilder.extractStyleContent(notification, contentBuilder, imageModelProvider) return contentBuilder.build().also { logger.logExtractionSucceeded(entry, it) } } -} -private fun Notification.title(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE) + private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? = + imageModelProvider.getImageModel(smallIcon, SmallSquare) + + private fun Notification.title(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE) -private fun Notification.text(): CharSequence? = extras?.getCharSequence(EXTRA_TEXT) + private fun Notification.text(): CharSequence? = extras?.getCharSequence(EXTRA_TEXT) -private fun Notification.subText(): String? = extras?.getString(EXTRA_SUB_TEXT) + private fun Notification.subText(): String? = extras?.getString(EXTRA_SUB_TEXT) -private fun Notification.shortCriticalText(): String? { - if (!android.app.Flags.apiRichOngoing()) { + private fun Notification.shortCriticalText(): String? { + if (!android.app.Flags.apiRichOngoing()) { + return null + } + if (this.shortCriticalText != null) { + return this.shortCriticalText + } + if (Flags.promoteNotificationsAutomatically()) { + return this.extras?.getString(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT) + } return null } - if (this.shortCriticalText != null) { - return this.shortCriticalText - } - if (Flags.promoteNotificationsAutomatically()) { - return this.extras?.getString(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT) - } - return null -} -private fun Notification.chronometerCountDown(): Boolean = - extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false + private fun Notification.chronometerCountDown(): Boolean = + extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false -private fun Notification.oldProgress(): OldProgress? { - val progress = progress() ?: return null - val max = progressMax() ?: return null - val isIndeterminate = progressIndeterminate() ?: return null - - return OldProgress(progress = progress, max = max, isIndeterminate = isIndeterminate) -} - -private fun Notification.progress(): Int? = extras?.getInt(EXTRA_PROGRESS) - -private fun Notification.progressMax(): Int? = extras?.getInt(EXTRA_PROGRESS_MAX) - -private fun Notification.progressIndeterminate(): Boolean? = - extras?.getBoolean(EXTRA_PROGRESS_INDETERMINATE) + private fun Notification.skeletonLargeIcon( + imageModelProvider: ImageModelProvider + ): ImageModel? = + getLargeIcon()?.let { + imageModelProvider.getImageModel(it, MediumSquare, skeletonImageTransform) + } -private fun Notification.extractWhen(): When? { - val time = `when` - val showsTime = showsTime() - val showsChronometer = showsChronometer() - val countDown = chronometerCountDown() + private fun Notification.oldProgress(): OldProgress? { + val progress = progress() ?: return null + val max = progressMax() ?: return null + val isIndeterminate = progressIndeterminate() ?: return null - return when { - showsTime -> When(time, When.Mode.BasicTime) - showsChronometer -> When(time, if (countDown) When.Mode.CountDown else When.Mode.CountUp) - else -> null + return OldProgress(progress = progress, max = max, isIndeterminate = isIndeterminate) } -} - -private fun Notification.Builder.extractStyleContent( - contentBuilder: PromotedNotificationContentModel.Builder -) { - val style = this.style - contentBuilder.style = - when (style) { - null -> Style.Base + private fun Notification.progress(): Int? = extras?.getInt(EXTRA_PROGRESS) - is BigPictureStyle -> { - style.extractContent(contentBuilder) - Style.BigPicture - } + private fun Notification.progressMax(): Int? = extras?.getInt(EXTRA_PROGRESS_MAX) - is BigTextStyle -> { - style.extractContent(contentBuilder) - Style.BigText - } + private fun Notification.progressIndeterminate(): Boolean? = + extras?.getBoolean(EXTRA_PROGRESS_INDETERMINATE) - is CallStyle -> { - style.extractContent(contentBuilder) - Style.Call - } + private fun Notification.extractWhen(): When? { + val time = `when` + val showsTime = showsTime() + val showsChronometer = showsChronometer() + val countDown = chronometerCountDown() - is ProgressStyle -> { - style.extractContent(contentBuilder) - Style.Progress - } + return when { + showsTime -> When(time, When.Mode.BasicTime) + showsChronometer -> + When(time, if (countDown) When.Mode.CountDown else When.Mode.CountUp) + else -> null + } + } - else -> Style.Ineligible + private fun Notification.skeletonVerificationIcon( + imageModelProvider: ImageModelProvider + ): ImageModel? = + extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon::class.java)?.let { + imageModelProvider.getImageModel(it, SmallSquare, skeletonImageTransform) } -} -private fun BigPictureStyle.extractContent( - contentBuilder: PromotedNotificationContentModel.Builder -) { - // TODO? -} + private fun Notification.verificationText(): CharSequence? = + extras.getCharSequence(EXTRA_VERIFICATION_TEXT) + + private fun Notification.Builder.extractStyleContent( + notification: Notification, + contentBuilder: PromotedNotificationContentModel.Builder, + imageModelProvider: ImageModelProvider, + ) { + val style = this.style + + contentBuilder.style = + when (style) { + null -> Style.Base + + is BigPictureStyle -> { + style.extractContent(contentBuilder) + Style.BigPicture + } + + is BigTextStyle -> { + style.extractContent(contentBuilder) + Style.BigText + } + + is CallStyle -> { + style.extractContent(notification, contentBuilder, imageModelProvider) + Style.Call + } + + is ProgressStyle -> { + style.extractContent(contentBuilder) + Style.Progress + } + + else -> Style.Ineligible + } + } -private fun BigTextStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { - // TODO? -} + private fun BigPictureStyle.extractContent( + contentBuilder: PromotedNotificationContentModel.Builder + ) { + // TODO? + } -private fun CallStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { - contentBuilder.personIcon = null // TODO - contentBuilder.personName = null // TODO - contentBuilder.verificationIcon = null // TODO - contentBuilder.verificationText = null // TODO -} + private fun BigTextStyle.extractContent( + contentBuilder: PromotedNotificationContentModel.Builder + ) { + // TODO? + } + + private fun CallStyle.extractContent( + notification: Notification, + contentBuilder: PromotedNotificationContentModel.Builder, + imageModelProvider: ImageModelProvider, + ) { + contentBuilder.personIcon = null // TODO + contentBuilder.personName = null // TODO + contentBuilder.verificationIcon = notification.skeletonVerificationIcon(imageModelProvider) + contentBuilder.verificationText = notification.verificationText() + } -private fun ProgressStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { - // TODO: Create NotificationProgressModel.toSkeleton, or something similar. - contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt()) + private fun ProgressStyle.extractContent( + contentBuilder: PromotedNotificationContentModel.Builder + ) { + // TODO: Create NotificationProgressModel.toSkeleton, or something similar. + contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt()) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index 58da5286dd71..af5a8203c979 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -19,11 +19,11 @@ package com.android.systemui.statusbar.notification.promoted.shared.model import android.annotation.DrawableRes import android.app.Notification import android.app.Notification.FLAG_PROMOTED_ONGOING -import android.graphics.drawable.Icon import androidx.annotation.ColorInt import com.android.internal.widget.NotificationProgressModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.notification.row.shared.ImageModel /** * The content needed to render a promoted notification to surfaces besides the notification stack, @@ -37,7 +37,7 @@ data class PromotedNotificationContentModel( * True if this notification was automatically promoted - see [AutomaticPromotionCoordinator]. */ val wasPromotedAutomatically: Boolean, - val skeletonSmallIcon: Icon?, // TODO(b/377568176): Make into an IconModel. + val smallIcon: ImageModel?, val appName: CharSequence?, val subText: CharSequence?, val shortCriticalText: String?, @@ -50,15 +50,15 @@ data class PromotedNotificationContentModel( @DrawableRes val profileBadgeResId: Int?, val title: CharSequence?, val text: CharSequence?, - val skeletonLargeIcon: Icon?, // TODO(b/377568176): Make into an IconModel. + val skeletonLargeIcon: ImageModel?, val oldProgress: OldProgress?, val colors: Colors, val style: Style, // for CallStyle: - val personIcon: Icon?, // TODO(b/377568176): Make into an IconModel. + val personIcon: ImageModel?, val personName: CharSequence?, - val verificationIcon: Icon?, // TODO(b/377568176): Make into an IconModel. + val verificationIcon: ImageModel?, val verificationText: CharSequence?, // for ProgressStyle: @@ -66,7 +66,7 @@ data class PromotedNotificationContentModel( ) { class Builder(val key: String) { var wasPromotedAutomatically: Boolean = false - var skeletonSmallIcon: Icon? = null + var smallIcon: ImageModel? = null var appName: CharSequence? = null var subText: CharSequence? = null var time: When? = null @@ -75,15 +75,15 @@ data class PromotedNotificationContentModel( @DrawableRes var profileBadgeResId: Int? = null var title: CharSequence? = null var text: CharSequence? = null - var skeletonLargeIcon: Icon? = null + var skeletonLargeIcon: ImageModel? = null var oldProgress: OldProgress? = null var style: Style = Style.Ineligible var colors: Colors = Colors(backgroundColor = 0, primaryTextColor = 0) // for CallStyle: - var personIcon: Icon? = null + var personIcon: ImageModel? = null var personName: CharSequence? = null - var verificationIcon: Icon? = null + var verificationIcon: ImageModel? = null var verificationText: CharSequence? = null // for ProgressStyle: @@ -93,7 +93,7 @@ data class PromotedNotificationContentModel( PromotedNotificationContentModel( identity = Identity(key, style), wasPromotedAutomatically = wasPromotedAutomatically, - skeletonSmallIcon = skeletonSmallIcon, + smallIcon = smallIcon, appName = appName, subText = subText, shortCriticalText = shortCriticalText, 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 15f316800000..053b4f5cb2ba 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 @@ -171,6 +171,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mIsPromotedOngoing = false; + @Nullable + public ImageModelIndex mImageModelIndex = null; + /** * Listener for when {@link ExpandableNotificationRow} is laid out. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 13ed6c449797..c7e15fdb98c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -61,6 +61,7 @@ import com.android.systemui.statusbar.notification.promoted.PromotedNotification import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder; @@ -437,6 +438,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder InflationProgress result = new InflationProgress(); final NotificationEntry entryForLogging = row.getEntry(); + // create an image inflater + result.mRowImageInflater = RowImageInflater.newInstance(row.mImageModelIndex); + if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view"); result.newContentView = createContentView(builder, bindParams.isMinimized); @@ -978,6 +982,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder NotificationContentView publicLayout = row.getPublicLayout(); logger.logAsyncTaskProgress(entry, "finishing"); + // Put the new image index on the row + row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex(); + if (PromotedNotificationContentModel.featureFlagEnabled()) { entry.setPromotedNotificationContentModel(result.mPromotedContent); } @@ -1363,15 +1370,20 @@ public class NotificationContentInflater implements NotificationRowContentBinder if (PromotedNotificationContentModel.featureFlagEnabled()) { mLogger.logAsyncTaskProgress(mEntry, "extracting promoted notification content"); + final ImageModelProvider imageModelProvider = + result.mRowImageInflater.useForContentModel(); final PromotedNotificationContentModel promotedContent = mPromotedNotificationContentExtractor.extractContent(mEntry, - recoveredBuilder); + recoveredBuilder, imageModelProvider); mLogger.logAsyncTaskProgress(mEntry, "extracted promoted notification content: " + promotedContent); result.mPromotedContent = promotedContent; } + mLogger.logAsyncTaskProgress(mEntry, "loading RON images"); + inflationProgress.mRowImageInflater.loadImagesSynchronously(packageContext); + mLogger.logAsyncTaskProgress(mEntry, "getting row image resolver (on wrong thread!)"); final NotificationInlineImageResolver imageResolver = mRow.getImageResolver(); @@ -1473,6 +1485,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder @VisibleForTesting static class InflationProgress { + RowImageInflater mRowImageInflater; + PromotedNotificationContentModel mPromotedContent; private RemoteViews newContentView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index f4aae6e288a7..bc3653a34fca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -490,6 +490,9 @@ constructor( } } + logger.logAsyncTaskProgress(entry, "loading RON images") + inflationProgress.rowImageInflater.loadImagesSynchronously(packageContext) + logger.logAsyncTaskProgress(entry, "getting row image resolver (on wrong thread!)") val imageResolver = row.imageResolver // wait for image resolver to finish preloading @@ -582,6 +585,7 @@ constructor( @VisibleForTesting class InflationProgress( @VisibleForTesting val packageContext: Context, + val rowImageInflater: RowImageInflater, val remoteViews: NewRemoteViews, val contentModel: NotificationContentModel, val promotedContent: PromotedNotificationContentModel?, @@ -674,15 +678,21 @@ constructor( promotedNotificationContentExtractor: PromotedNotificationContentExtractor, logger: NotificationRowContentBinderLogger, ): InflationProgress { + val rowImageInflater = + RowImageInflater.newInstance(previousIndex = row.mImageModelIndex) + val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { logger.logAsyncTaskProgress(entry, "extracting promoted notification content") - promotedNotificationContentExtractor.extractContent(entry, builder).also { - logger.logAsyncTaskProgress( - entry, - "extracted promoted notification content: $it", - ) - } + val imageModelProvider = rowImageInflater.useForContentModel() + promotedNotificationContentExtractor + .extractContent(entry, builder, imageModelProvider) + .also { + logger.logAsyncTaskProgress( + entry, + "extracted promoted notification content: $it", + ) + } } else { null } @@ -719,7 +729,7 @@ constructor( builder = builder, systemUiContext = systemUiContext, redactText = false, - summarization = entry.ranking.summarization + summarization = entry.ranking.summarization, ) } else null @@ -736,7 +746,7 @@ constructor( builder = builder, systemUiContext = systemUiContext, redactText = true, - summarization = null + summarization = null, ) } else { SingleLineViewInflater.inflateRedactedSingleLineViewModel( @@ -761,6 +771,7 @@ constructor( return InflationProgress( packageContext = packageContext, + rowImageInflater = rowImageInflater, remoteViews = remoteViews, contentModel = contentModel, promotedContent = promotedContent, @@ -1474,6 +1485,9 @@ constructor( } logger.logAsyncTaskProgress(entry, "finishing") + // Put the new image index on the row + row.mImageModelIndex = result.rowImageInflater.getNewImageIndex() + entry.setContentModel(result.contentModel) if (PromotedNotificationContentModel.featureFlagEnabled()) { entry.promotedNotificationContentModel = result.promotedContent diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt new file mode 100644 index 000000000000..95ef60fdcefe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowImageInflater.kt @@ -0,0 +1,216 @@ +/* + * 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.row + +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import androidx.annotation.VisibleForTesting +import com.android.app.tracing.traceSection +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod +import com.android.systemui.statusbar.notification.row.shared.IconData +import com.android.systemui.statusbar.notification.row.shared.ImageModel +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageTransform +import java.util.Date + +/** + * A class used as part of the notification content inflation process to generate image models, + * resolve image content for active models, and manage a generational index of images to reduce + * image load overhead. + * + * Each round of inflation follows this process: + * 1. Instantiate a [newInstance] of this class using the current index. + * 2. Call [useForContentModel] and use that object to generate [ImageModel] objects. + * 3. [ImageModel] objects may be stored in a content model, which may be used in Flows or States. + * 4. On the background thread, call [loadImagesSynchronously] to ensure all models have images. + * 5. In case of success, call [getNewImageIndex] and if the result is not null, replace the + * original index with this one that is a generation newer. In case the inflation failed, the + * [RowImageInflater] can be discarded and while all newly resolved images will be discarded, no + * change will have been made to the previous index. + */ +interface RowImageInflater { + /** + * This returns an [ImageModelProvider] that can be used for getting models of images for use in + * a content model. Calling this method marks the inflater as being used, which means that + * instead of [getNewImageIndex] returning the previous index, it will now suddenly return + * nothing unless until other models are provided. This behavior allows the implicit absence of + * calls to evict unused images from the new index. + * + * NOTE: right now there is only one inflation process which uses this to access images. In the + * future we will likely have more. In that case, we will need this method and the + * [ImageModelIndex] to support the concept of different optional inflation lanes. + * + * Here's an example to illustrate why this would be necessary: + * 1. We inflate just the general content and save the index with 6 images. + * 2. Later, we inflate just the AOD RON content and save the index with 3 images, discarding + * the 3 from the general content. + * 3. Later, we reinflate the general content and have to reload 3 images that should've been in + * the index. + */ + fun useForContentModel(): ImageModelProvider + + /** + * Synchronously load all drawables that are not in the index, and ensure the [ImageModel]s + * previously returned by an [ImageModelProvider] all provide access to those drawables. + */ + fun loadImagesSynchronously(context: Context) + + /** + * Get the next generation of the [ImageModelIndex] for this row. This will return the previous + * index if this inflater was never used. + */ + fun getNewImageIndex(): ImageModelIndex? + + companion object { + @Suppress("NOTHING_TO_INLINE") + @JvmStatic + inline fun featureFlagEnabled() = PromotedNotificationUiAod.isEnabled + + @JvmStatic + fun newInstance(previousIndex: ImageModelIndex?): RowImageInflater = + if (featureFlagEnabled()) { + RowImageInflaterImpl(previousIndex) + } else { + RowImageInflaterStub + } + } +} + +/** A no-op implementation that does nothing */ +private object ImageModelProviderStub : ImageModelProvider { + override fun getImageModel( + icon: Icon, + sizeClass: ImageSizeClass, + transform: ImageTransform, + ): ImageModel? = null +} + +/** A no-op implementation that does nothing */ +private object RowImageInflaterStub : RowImageInflater { + override fun useForContentModel(): ImageModelProvider = ImageModelProviderStub + + override fun loadImagesSynchronously(context: Context) = Unit + + override fun getNewImageIndex(): ImageModelIndex? = null +} + +class RowImageInflaterImpl(private val previousIndex: ImageModelIndex?) : RowImageInflater { + private val providedImages = mutableListOf<LazyImage>() + + /** + * For now there is only one way we use this, so we don't need to track which "model" it was + * used for. If in the future we use it for more models, then we can do that, and also track the + * parts of the index that should or shouldn't be copied. + */ + private var wasUsed = false + + /** Gets the ImageModelProvider that is used for inflating the content model. */ + override fun useForContentModel(): ImageModelProvider { + wasUsed = true + return object : ImageModelProvider { + override fun getImageModel( + icon: Icon, + sizeClass: ImageSizeClass, + transform: ImageTransform, + ): ImageModel? { + val iconData = IconData.fromIcon(icon) ?: return null + // if we've already provided an equivalent image, return it again. + providedImages.firstOrNull(iconData, sizeClass, transform)?.let { + return it + } + // create and return a new entry + return LazyImage(iconData, sizeClass, transform).also { newImage -> + // ensure all entries are stored + providedImages.add(newImage) + // load the image result from the index into our new object + previousIndex?.findImage(iconData, sizeClass, transform)?.let { + // copy the result into our new object + newImage.result = it + } + } + } + } + } + + override fun loadImagesSynchronously(context: Context) { + traceSection("RowImageInflater.loadImageDrawablesSync") { + providedImages.forEach { lazyImage -> + if (lazyImage.result == null) { + lazyImage.result = lazyImage.load(context) + } + } + } + } + + private fun LazyImage.load(context: Context): ImageResult { + traceSection("LazyImage.load") { + // TODO: use the sizeClass to load the drawable into a correctly sized bitmap, + // and be sure to respect [lazyImage.transform.requiresSoftwareBitmapInput] + val iconDrawable = + icon.toIcon().loadDrawable(context) + ?: return ImageResult.Empty("Icon.loadDrawable() returned null for $icon") + return transform.transformDrawable(iconDrawable)?.let { ImageResult.Image(it) } + ?: return ImageResult.Empty("Transform ${transform.key} returned null") + } + } + + override fun getNewImageIndex(): ImageModelIndex? = + if (wasUsed) ImageModelIndex(providedImages) else previousIndex +} + +class ImageModelIndex internal constructor(data: Collection<LazyImage>) { + private val images = data.toMutableList() + + fun findImage( + icon: IconData, + sizeClass: ImageSizeClass, + transform: ImageTransform, + ): ImageResult? = images.firstOrNull(icon, sizeClass, transform)?.result + + @VisibleForTesting + val contentsForTesting: MutableList<LazyImage> + get() = images +} + +private fun Collection<LazyImage>.firstOrNull( + icon: IconData, + sizeClass: ImageSizeClass, + transform: ImageTransform, +): LazyImage? = firstOrNull { + it.sizeClass == sizeClass && it.icon == icon && it.transform == transform +} + +data class LazyImage( + val icon: IconData, + val sizeClass: ImageSizeClass, + val transform: ImageTransform, + var result: ImageResult? = null, +) : ImageModel { + override val drawable: Drawable? + get() = (result as? ImageResult.Image)?.drawable +} + +/** The result of attempting to load an image. */ +sealed interface ImageResult { + /** Indicates a null result from the image loading process, with a reason for debugging */ + data class Empty(val reason: String, val time: Date = Date()) : ImageResult + + /** Stores the drawable result of loading an image */ + data class Image(val drawable: Drawable) : ImageResult +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconData.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconData.kt new file mode 100644 index 000000000000..7120abcbaf24 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconData.kt @@ -0,0 +1,145 @@ +/* + * 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.row.shared + +import android.annotation.IdRes +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.BlendMode +import android.graphics.drawable.Icon +import android.net.Uri + +/** + * This is a representation of an [Icon] that supports the [equals] and [hashCode] semantics + * required for use as a map key, or for change detection. + */ +sealed class IconData { + abstract fun toIcon(): Icon + + class BitmapIcon(val sourceBitmap: Bitmap, val isAdaptive: Boolean, val tint: IconTint?) : + IconData() { + override fun toIcon(): Icon = + if (isAdaptive) { + Icon.createWithAdaptiveBitmap(sourceBitmap) + } else { + Icon.createWithBitmap(sourceBitmap) + } + .withTint(tint) + + override fun equals(other: Any?): Boolean = + when (other) { + null -> false + (other === this) -> true + !is BitmapIcon -> false + else -> + other.isAdaptive == isAdaptive && + other.tint == tint && + other.sourceBitmap.sameAs(sourceBitmap) + } + + override fun hashCode(): Int { + var result = sourceBitmap.width + result = 31 * result + sourceBitmap.height + result = 31 * result + isAdaptive.hashCode() + result = 31 * result + (tint?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "BitmapIcon(sourceBitmap=$sourceBitmap, isAdaptive=$isAdaptive, tint=$tint)" + } + + data class ResourceIcon(val packageName: String, @IdRes val resId: Int, val tint: IconTint?) : + IconData() { + @SuppressLint("ResourceType") + override fun toIcon(): Icon = Icon.createWithResource(packageName, resId).withTint(tint) + } + + class DataIcon(val data: ByteArray, val tint: IconTint?) : IconData() { + override fun toIcon(): Icon = Icon.createWithData(data, 0, data.size).withTint(tint) + + override fun equals(other: Any?): Boolean = + when (other) { + null -> false + (other === this) -> true + !is DataIcon -> false + else -> other.data.contentEquals(data) && other.tint == tint + } + + override fun hashCode(): Int { + var result = data.contentHashCode() + result = 31 * result + (tint?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "DataIcon(data.size=${data.size}, data.hashCode=${data.contentHashCode()}, tint=$tint)" + } + + data class UriIcon(val uri: Uri, val isAdaptive: Boolean, val tint: IconTint?) : IconData() { + + override fun toIcon(): Icon = + if (isAdaptive) { + Icon.createWithAdaptiveBitmapContentUri(uri) + } else { + Icon.createWithContentUri(uri) + } + .withTint(tint) + } + + companion object { + fun fromIcon(icon: Icon): IconData? { + val tint = icon.tintList?.let { tintList -> IconTint(tintList, icon.tintBlendMode) } + return when (icon.type) { + Icon.TYPE_BITMAP -> + icon.bitmap?.let { bitmap -> BitmapIcon(bitmap, isAdaptive = false, tint) } + Icon.TYPE_ADAPTIVE_BITMAP -> + icon.bitmap?.let { bitmap -> BitmapIcon(bitmap, isAdaptive = true, tint) } + Icon.TYPE_URI -> UriIcon(icon.uri, isAdaptive = false, tint) + Icon.TYPE_URI_ADAPTIVE_BITMAP -> UriIcon(icon.uri, isAdaptive = true, tint) + Icon.TYPE_RESOURCE -> ResourceIcon(icon.resPackage, icon.resId, tint) + Icon.TYPE_DATA -> icon.safeData?.let { data -> DataIcon(data, tint) } + else -> null + } + } + + private val Icon.safeData: ByteArray? + get() { + val dataBytes = dataBytes + val dataLength = dataLength + val dataOffset = dataOffset + if (dataOffset == 0 && dataLength == dataBytes.size) { + return dataBytes + } + if (dataLength < dataBytes.size - dataOffset) { + return null + } + return dataBytes.copyOfRange(dataOffset, dataOffset + dataLength) + } + + private fun Icon.withTint(tint: IconTint?): Icon { + if (tint != null) { + tintList = tint.tintList + tintBlendMode = tint.blendMode + } + return this + } + } +} + +data class IconTint(val tintList: ColorStateList, val blendMode: BlendMode) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ImageModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ImageModel.kt new file mode 100644 index 000000000000..6a1533f97fca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ImageModel.kt @@ -0,0 +1,42 @@ +/* + * 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.row.shared + +import android.graphics.drawable.Drawable + +/** + * This object can provides access to a drawable in the future. + * + * Additionally, all implementations must provide stable equality which means that you can compare + * to other instances before the drawable has been resolved. + * + * This means you can use these in fields of a Model that is stored in State or StateFlow object to + * provide access to a drawable while still debouncing duplicates. + */ +interface ImageModel { + /** The image, once resolved. */ + val drawable: Drawable? + + /** Returns whether this model does not currently provide access to an image. */ + fun isEmpty() = drawable == null + + /** Returns whether this model currently provides access to an image. */ + fun isNotEmpty() = drawable != null +} + +/** Returns whether this model is null or does not currently provide access to an image. */ +fun ImageModel?.isNullOrEmpty() = this == null || this.isEmpty() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ImageModelProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ImageModelProvider.kt new file mode 100644 index 000000000000..8eadf566d146 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ImageModelProvider.kt @@ -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 com.android.systemui.statusbar.notification.row.shared + +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon + +/** + * An interface which allows the background inflation process, when loading content from a + * notification, to acquire a [ImageModel] that can be used later in the inflation process to access + * the actual [Drawable] which it represents. + */ +interface ImageModelProvider { + /** + * Defines the rough size we expect the image to be. We use this abstraction to reduce memory + * footprint without coupling image loading to the view layer. + */ + enum class ImageSizeClass { + /** Roughly 24dp */ + SmallSquare, + + /** Around 48dp */ + MediumSquare, + + /** About as wide as a notification. */ + LargeWide, + } + + /** + * This is the base class for an image transform that allows the loaded icon to be altered in + * some way (other than resizing) prior to being indexed. This transform will be used as part of + * the index key. + */ + abstract class ImageTransform(val key: String) { + open val requiresSoftwareBitmapInput: Boolean = false + + abstract fun transformDrawable(input: Drawable): Drawable? + + final override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (other !is ImageTransform) return false + return key == other.key + } + + final override fun hashCode(): Int { + return key.hashCode() + } + } + + /** The default passthrough transform for images */ + object IdentityImageTransform : ImageTransform("Identity") { + override fun transformDrawable(input: Drawable) = input + } + + /** Returns an [ImageModel] which will provide access to a [Drawable] in the future. */ + fun getImageModel( + icon: Icon, + sizeClass: ImageSizeClass, + transform: ImageTransform = IdentityImageTransform, + ): ImageModel? +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/SkeletonImageTransform.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/SkeletonImageTransform.kt new file mode 100644 index 000000000000..d69e546f640d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/SkeletonImageTransform.kt @@ -0,0 +1,45 @@ +/* + * 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.row.shared + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import com.android.internal.util.ContrastColorUtil +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeDisplayAware +import javax.inject.Inject + +/** + * An [ImageModelProvider.ImageTransform] which acts as a filter to only return a drawable that is + * grayscale, and tints it to white for display on the AOD. + */ +@SysUISingleton +class SkeletonImageTransform @Inject constructor(@ShadeDisplayAware context: Context) : + ImageModelProvider.ImageTransform("Skeleton") { + + override val requiresSoftwareBitmapInput: Boolean = true + + private val contrastColorUtil = ContrastColorUtil.getInstance(context) + + override fun transformDrawable(input: Drawable): Drawable? { + return input + .takeIf { contrastColorUtil.isGrayscaleIcon(it) } + ?.mutate() + ?.apply { setTint(Color.WHITE) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index d6b34b068cc5..42d02e10ab8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -26,7 +26,6 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_ import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL; import static com.android.systemui.Flags.magneticNotificationSwipes; import static com.android.systemui.Flags.notificationOverExpansionClippingFix; -import static com.android.systemui.Flags.notificationsRedesignFooterView; import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE; import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_DOWN; @@ -695,28 +694,11 @@ public class NotificationStackScrollLayout protected void onFinishInflate() { super.onFinishInflate(); - if (notificationsRedesignFooterView()) { - int bottomMargin = getBottomMargin(getContext()); - MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); - lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin); - setLayoutParams(lp); - } - if (!ModesEmptyShadeFix.isEnabled()) { inflateEmptyShadeView(); } } - /** Get the pixel value of the bottom margin, taking flags into account. */ - public static int getBottomMargin(Context context) { - Resources res = context.getResources(); - if (notificationsRedesignFooterView()) { - return res.getDimensionPixelSize(R.dimen.notification_2025_panel_margin_bottom); - } else { - return res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom); - } - } - /** * Sets whether keyguard bypass is enabled. If true, this layout will be rendered in bypass * mode when it is on the keyguard. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 673eb9f85ec3..06b989aaab57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -107,7 +107,7 @@ public class StackScrollAlgorithm { mGapHeightOnLockscreen = res.getDimensionPixelSize( R.dimen.notification_section_divider_height_lockscreen); mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings); - mMarginBottom = NotificationStackScrollLayout.getBottomMargin(context); + mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom); mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context); mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small); mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt index c8b3341a0240..107875df6bfa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -26,7 +26,6 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.policy.SplitShadeStateController import dagger.Lazy import javax.inject.Inject @@ -80,7 +79,8 @@ constructor( getBoolean(R.bool.config_use_large_screen_shade_header), marginHorizontal = getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal), - marginBottom = NotificationStackScrollLayout.getBottomMargin(context), + marginBottom = + getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top), marginTopLargeScreen = largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 1abf8ab2cadc..75e89a676d19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -81,7 +81,6 @@ import com.android.systemui.shade.shared.model.ShadeMode.Dual import com.android.systemui.shade.shared.model.ShadeMode.Single import com.android.systemui.shade.shared.model.ShadeMode.Split import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -268,7 +267,8 @@ constructor( horizontalPosition = horizontalPosition, marginStart = if (shadeMode is Split) 0 else marginHorizontal, marginEnd = marginHorizontal, - marginBottom = NotificationStackScrollLayout.getBottomMargin(context), + marginBottom = + getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), // y position of the NSSL in the window needs to be 0 under scene // container marginTop = 0, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 3de7db76deae..9abe9aa5e598 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -16,8 +16,6 @@ package com.android.systemui.shade -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -28,7 +26,6 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService @@ -125,7 +122,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) - overrideResource(R.dimen.notification_2025_panel_margin_bottom, NOTIFICATIONS_MARGIN) overrideResource(R.bool.config_use_split_notification_shade, false) overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) @@ -364,7 +360,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) fun testNotificationsMarginBottomIsUpdated() { Mockito.clearInvocations(view) enableSplitShade() @@ -376,18 +371,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) - fun testNotificationsMarginBottomIsUpdated_footerRedesign() { - Mockito.clearInvocations(view) - enableSplitShade() - verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) - - overrideResource(R.dimen.notification_2025_panel_margin_bottom, 100) - disableSplitShade() - verify(view).setNotificationsMarginBottom(100) - } - - @Test fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { enableSplitShade() underTest.updateResources() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index 552fe8d5bc55..4c12cc886e33 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -16,8 +16,6 @@ package com.android.systemui.shade -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -28,7 +26,6 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService @@ -125,7 +122,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) - overrideResource(R.dimen.notification_2025_panel_margin_bottom, NOTIFICATIONS_MARGIN) overrideResource(R.bool.config_use_split_notification_shade, false) overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) @@ -362,7 +358,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) fun testNotificationsMarginBottomIsUpdated() { Mockito.clearInvocations(view) enableSplitShade() @@ -374,18 +369,6 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW) - fun testNotificationsMarginBottomIsUpdated_footerRedesign() { - Mockito.clearInvocations(view) - enableSplitShade() - verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) - - overrideResource(R.dimen.notification_2025_panel_margin_bottom, 100) - disableSplitShade() - verify(view).setNotificationsMarginBottom(100) - } - - @Test fun testSplitShadeLayout_isAlignedToGuideline() { enableSplitShade() underTest.updateResources() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt index 680e0de22794..8fdf5dbf2aeb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Notification import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider import org.junit.Assert class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtractor { @@ -29,6 +30,7 @@ class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtr override fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, + imageModelProvider: ImageModelProvider, ): PromotedNotificationContentModel? { extractCalls.add(entry to recoveredBuilder) @@ -38,13 +40,13 @@ class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtr } else { // If entries *are* set, fail on unexpected ones. Assert.assertTrue(contentForEntry.containsKey(entry)) - return contentForEntry.get(entry) + return contentForEntry[entry] } } fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModel?) { contentForEntry.clear() - contentForEntry.put(entry, content) + contentForEntry[entry] = content extractCalls.clear() } 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 912d5027c494..63521de096c9 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 @@ -18,8 +18,13 @@ package com.android.systemui.statusbar.notification.promoted import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform var Kosmos.promotedNotificationContentExtractor by Kosmos.Fixture { - PromotedNotificationContentExtractorImpl(applicationContext, promotedNotificationLogger) + PromotedNotificationContentExtractorImpl( + applicationContext, + skeletonImageTransform, + promotedNotificationLogger, + ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index 6246d986f9e5..e9c6c18550fb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -78,6 +78,7 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProviderImpl import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProviderImpl import com.android.systemui.statusbar.notification.row.icon.NotificationRowIconViewInflaterFactory import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor +import com.android.systemui.statusbar.notification.row.shared.SkeletonImageTransform import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.KeyguardDismissUtil @@ -224,6 +225,7 @@ class ExpandableNotificationRowBuilder( val promotedNotificationContentExtractor = PromotedNotificationContentExtractorImpl( context, + SkeletonImageTransform(context), PromotedNotificationLogger(logcatLogBuffer("PromotedNotifLog")), ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/shared/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/shared/PromotedNotificationContentExtractorKosmos.kt new file mode 100644 index 000000000000..16688235cdbb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/shared/PromotedNotificationContentExtractorKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.row.shared + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +var Kosmos.skeletonImageTransform by Kosmos.Fixture { SkeletonImageTransform(applicationContext) } diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index 1ba574559918..0354d2be60c9 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -25,8 +25,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; +import android.widget.LinearLayout; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.android.internal.R; @@ -40,12 +42,44 @@ public class AutoclickTypePanel { private final WindowManager mWindowManager; + // Whether the panel is expanded or not. + private boolean mExpanded = false; + + private final LinearLayout mLeftClickButton; + private final LinearLayout mRightClickButton; + private final LinearLayout mDoubleClickButton; + private final LinearLayout mDragButton; + private final LinearLayout mScrollButton; + public AutoclickTypePanel(Context context, WindowManager windowManager) { mContext = context; mWindowManager = windowManager; - mContentView = LayoutInflater.from(context).inflate( - R.layout.accessibility_autoclick_type_panel, null); + mContentView = + LayoutInflater.from(context) + .inflate(R.layout.accessibility_autoclick_type_panel, null); + mLeftClickButton = + mContentView.findViewById(R.id.accessibility_autoclick_left_click_layout); + mRightClickButton = + mContentView.findViewById(R.id.accessibility_autoclick_right_click_layout); + mDoubleClickButton = + mContentView.findViewById(R.id.accessibility_autoclick_double_click_layout); + mScrollButton = mContentView.findViewById(R.id.accessibility_autoclick_scroll_layout); + mDragButton = mContentView.findViewById(R.id.accessibility_autoclick_drag_layout); + + initializeButtonState(); + } + + private void initializeButtonState() { + mLeftClickButton.setOnClickListener(v -> togglePanelExpansion(mLeftClickButton)); + mRightClickButton.setOnClickListener(v -> togglePanelExpansion(mRightClickButton)); + mDoubleClickButton.setOnClickListener(v -> togglePanelExpansion(mDoubleClickButton)); + mScrollButton.setOnClickListener(v -> togglePanelExpansion(mScrollButton)); + mDragButton.setOnClickListener(v -> togglePanelExpansion(mDragButton)); + + // Initializes panel as collapsed state and only displays the left click button. + hideAllClickTypeButtons(); + mLeftClickButton.setVisibility(View.VISIBLE); } public void show() { @@ -56,6 +90,51 @@ public class AutoclickTypePanel { mWindowManager.removeView(mContentView); } + /** Toggles the panel expanded or collapsed state. */ + private void togglePanelExpansion(LinearLayout button) { + if (mExpanded) { + // If the panel is already in expanded state, we should collapse it by hiding all + // buttons except the one user selected. + hideAllClickTypeButtons(); + button.setVisibility(View.VISIBLE); + } else { + // If the panel is already collapsed, we just need to expand it. + showAllClickTypeButtons(); + } + + // Toggle the state. + mExpanded = !mExpanded; + } + + /** Hide all buttons on the panel except pause and position buttons. */ + private void hideAllClickTypeButtons() { + mLeftClickButton.setVisibility(View.GONE); + mRightClickButton.setVisibility(View.GONE); + mDoubleClickButton.setVisibility(View.GONE); + mDragButton.setVisibility(View.GONE); + mScrollButton.setVisibility(View.GONE); + } + + /** Show all buttons on the panel except pause and position buttons. */ + private void showAllClickTypeButtons() { + mLeftClickButton.setVisibility(View.VISIBLE); + mRightClickButton.setVisibility(View.VISIBLE); + mDoubleClickButton.setVisibility(View.VISIBLE); + mDragButton.setVisibility(View.VISIBLE); + mScrollButton.setVisibility(View.VISIBLE); + } + + @VisibleForTesting + boolean getExpansionStateForTesting() { + return mExpanded; + } + + @VisibleForTesting + @NonNull + View getContentViewForTesting() { + return mContentView; + } + /** * Retrieves the layout params for AutoclickIndicatorView, used when it's added to the Window * Manager. 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 c93f107d6640..0b8b115e65d0 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -35,7 +35,6 @@ import android.hardware.tv.mediaquality.IPictureProfileChangedListener; import android.hardware.tv.mediaquality.ISoundProfileAdjustmentListener; import android.hardware.tv.mediaquality.ISoundProfileChangedListener; import android.hardware.tv.mediaquality.ParamCapability; -import android.hardware.tv.mediaquality.ParameterRange; import android.hardware.tv.mediaquality.PictureParameter; import android.hardware.tv.mediaquality.PictureParameters; import android.hardware.tv.mediaquality.SoundParameter; @@ -103,8 +102,8 @@ public class MediaQualityService extends SystemService { private final BiMap<Long, String> mPictureProfileTempIdMap; private final BiMap<Long, String> mSoundProfileTempIdMap; private IMediaQuality mMediaQuality; - private IPictureProfileAdjustmentListener mPpAdjustmentListener; - private ISoundProfileAdjustmentListener mSpAdjustmentListener; + private PictureProfileAdjustmentListenerImpl mPictureProfileAdjListener; + private SoundProfileAdjustmentListenerImpl mSoundProfileAdjListener; private IPictureProfileChangedListener mPpChangedListener; private ISoundProfileChangedListener mSpChangedListener; private final HalAmbientBacklightCallback mHalAmbientBacklightCallback; @@ -113,6 +112,9 @@ public class MediaQualityService extends SystemService { private final SparseArray<UserState> mUserStates = new SparseArray<>(); private SharedPreferences mPictureProfileSharedPreference; private SharedPreferences mSoundProfileSharedPreference; + private HalNotifier mHalNotifier; + private MqManagerNotifier mMqManagerNotifier; + private MqDatabaseUtils mMqDatabaseUtils; // A global lock for picture profile objects. private final Object mPictureProfileLock = new Object(); @@ -125,12 +127,17 @@ public class MediaQualityService extends SystemService { super(context); mContext = context; mHalAmbientBacklightCallback = new HalAmbientBacklightCallback(); + mPictureProfileAdjListener = new PictureProfileAdjustmentListenerImpl(mContext); + mSoundProfileAdjListener = new SoundProfileAdjustmentListenerImpl(mContext); mPackageManager = mContext.getPackageManager(); mPictureProfileTempIdMap = new BiMap<>(); mSoundProfileTempIdMap = new BiMap<>(); mMediaQualityDbHelper = new MediaQualityDbHelper(mContext); + mMqDatabaseUtils = new MqDatabaseUtils(mContext); mMediaQualityDbHelper.setWriteAheadLoggingEnabled(true); mMediaQualityDbHelper.setIdleConnectionTimeout(30); + mHalNotifier = new HalNotifier(); + mMqManagerNotifier = new MqManagerNotifier(); // The package info in the context isn't initialized in the way it is for normal apps, // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we @@ -155,90 +162,12 @@ public class MediaQualityService extends SystemService { } Slogf.d(TAG, "Binder is not null"); - mPpAdjustmentListener = new IPictureProfileAdjustmentListener.Stub() { - @Override - public void onPictureProfileAdjusted( - android.hardware.tv.mediaquality.PictureProfile pictureProfile) - throws RemoteException { - // TODO - } - - @Override - public void onParamCapabilityChanged(long pictureProfileId, ParamCapability[] caps) - throws RemoteException { - // TODO - } - - @Override - public void onVendorParamCapabilityChanged(long pictureProfileId, - VendorParamCapability[] caps) throws RemoteException { - // TODO - } - - @Override - public void requestPictureParameters(long pictureProfileId) throws RemoteException { - // TODO - } - - @Override - public void onStreamStatusChanged(long pictureProfileId, byte status) - throws RemoteException { - // TODO - } - - @Override - public int getInterfaceVersion() throws RemoteException { - return 0; - } - - @Override - public String getInterfaceHash() throws RemoteException { - return null; - } - }; - mSpAdjustmentListener = new ISoundProfileAdjustmentListener.Stub() { - - @Override - public void onSoundProfileAdjusted( - android.hardware.tv.mediaquality.SoundProfile soundProfile) - throws RemoteException { - // TODO - } - - @Override - public void onParamCapabilityChanged(long soundProfileId, ParamCapability[] caps) - throws RemoteException { - // TODO - } - - @Override - public void onVendorParamCapabilityChanged(long soundProfileId, - VendorParamCapability[] caps) throws RemoteException { - // TODO - } - - @Override - public void requestSoundParameters(long soundProfileId) throws RemoteException { - // TODO - } - - @Override - public int getInterfaceVersion() throws RemoteException { - return 0; - } - - @Override - public String getInterfaceHash() throws RemoteException { - return null; - } - }; - mMediaQuality = IMediaQuality.Stub.asInterface(binder); if (mMediaQuality != null) { try { mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback); - mMediaQuality.setPictureProfileAdjustmentListener(mPpAdjustmentListener); - mMediaQuality.setSoundProfileAdjustmentListener(mSpAdjustmentListener); + mMediaQuality.setPictureProfileAdjustmentListener(mPictureProfileAdjListener); + mMediaQuality.setSoundProfileAdjustmentListener(mSoundProfileAdjListener); } catch (RemoteException e) { Slog.e(TAG, "Failed to set ambient backlight detector callback", e); } @@ -259,7 +188,8 @@ public class MediaQualityService extends SystemService { if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -280,43 +210,19 @@ public class MediaQualityService extends SystemService { MediaQualityUtils.populateTempIdMap(mPictureProfileTempIdMap, id); String value = mPictureProfileTempIdMap.getValue(id); pp.setProfileId(value); - notifyOnPictureProfileAdded(value, pp, Binder.getCallingUid(), + mMqManagerNotifier.notifyOnPictureProfileAdded(value, pp, Binder.getCallingUid(), Binder.getCallingPid()); return pp; } } - private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) { - // TODO: only notify HAL when the profile is active / being used - try { - mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId, - params)); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to notify HAL on picture profile change.", e); - } - } - - private android.hardware.tv.mediaquality.PictureProfile convertToHalPictureProfile(Long id, - PersistableBundle params) { - PictureParameters pictureParameters = new PictureParameters(); - pictureParameters.pictureParameters = - MediaQualityUtils.convertPersistableBundleToPictureParameterList( - params); - - android.hardware.tv.mediaquality.PictureProfile toReturn = - new android.hardware.tv.mediaquality.PictureProfile(); - toReturn.pictureProfileId = id; - toReturn.parameters = pictureParameters; - - return toReturn; - } - @GuardedBy("mPictureProfileLock") @Override public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) { Long dbId = mPictureProfileTempIdMap.getKey(id); if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { - notifyOnPictureProfileError(id, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -328,17 +234,12 @@ public class MediaQualityService extends SystemService { pp.getInputId(), pp.getParameters()); - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - db.replace(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - null, values); - notifyOnPictureProfileUpdated(mPictureProfileTempIdMap.getValue(dbId), - getPictureProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid()); - notifyHalOnPictureProfileChange(dbId, pp.getParameters()); + updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters()); } } private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) { - PictureProfile fromDb = getPictureProfile(dbId); + PictureProfile fromDb = mMqDatabaseUtils.getPictureProfile(dbId); return fromDb.getProfileType() == toUpdate.getProfileType() && fromDb.getPackageName().equals(toUpdate.getPackageName()) && fromDb.getName().equals(toUpdate.getName()) @@ -351,9 +252,10 @@ public class MediaQualityService extends SystemService { synchronized (mPictureProfileLock) { Long dbId = mPictureProfileTempIdMap.getKey(id); - PictureProfile toDelete = getPictureProfile(dbId); + PictureProfile toDelete = mMqDatabaseUtils.getPictureProfile(dbId); if (!hasPermissionToRemovePictureProfile(toDelete)) { - notifyOnPictureProfileError(id, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -364,13 +266,15 @@ public class MediaQualityService extends SystemService { int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, selection, selectionArgs); if (result == 0) { - notifyOnPictureProfileError(id, PictureProfile.ERROR_INVALID_ARGUMENT, + mMqManagerNotifier.notifyOnPictureProfileError(id, + PictureProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); } - notifyOnPictureProfileRemoved(mPictureProfileTempIdMap.getValue(dbId), toDelete, + mMqManagerNotifier.notifyOnPictureProfileRemoved( + mPictureProfileTempIdMap.getValue(dbId), toDelete, Binder.getCallingUid(), Binder.getCallingPid()); mPictureProfileTempIdMap.remove(dbId); - notifyHalOnPictureProfileChange(dbId, null); + mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); } } } @@ -395,7 +299,7 @@ public class MediaQualityService extends SystemService { synchronized (mPictureProfileLock) { try ( - Cursor cursor = getCursorAfterQuerying( + Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, MediaQualityUtils.getMediaProfileColumns(includeParams), selection, selectionArguments) @@ -418,38 +322,13 @@ public class MediaQualityService extends SystemService { } } - private PictureProfile getPictureProfile(Long dbId) { - String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArguments = {Long.toString(dbId)}; - - try ( - Cursor cursor = getCursorAfterQuerying( - mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, - MediaQualityUtils.getMediaProfileColumns(false), selection, - selectionArguments) - ) { - int count = cursor.getCount(); - if (count == 0) { - return null; - } - if (count > 1) { - Log.wtf(TAG, TextUtils.formatSimple(String.valueOf(Locale.US), "%d entries " - + "found for id=%d in %s. Should only ever be 0 or 1.", - count, dbId, mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME)); - return null; - } - cursor.moveToFirst(); - return MediaQualityUtils.convertCursorToPictureProfileWithTempId(cursor, - mPictureProfileTempIdMap); - } - } - @GuardedBy("mPictureProfileLock") @Override public List<PictureProfile> getPictureProfilesByPackage( String packageName, Bundle options, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -458,7 +337,7 @@ public class MediaQualityService extends SystemService { options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; - return getPictureProfilesBasedOnConditions(MediaQualityUtils + return mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils .getMediaProfileColumns(includeParams), selection, selectionArguments); } @@ -478,11 +357,12 @@ public class MediaQualityService extends SystemService { @Override public boolean setDefaultPictureProfile(String profileId, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(profileId, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(profileId, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } - PictureProfile pictureProfile = getPictureProfile( + PictureProfile pictureProfile = mMqDatabaseUtils.getPictureProfile( mPictureProfileTempIdMap.getKey(profileId)); PersistableBundle params = pictureProfile.getParameters(); @@ -507,13 +387,14 @@ public class MediaQualityService extends SystemService { @Override public List<String> getPictureProfilePackageNames(UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } String [] column = {BaseParameters.PARAMETER_PACKAGE}; synchronized (mPictureProfileLock) { - List<PictureProfile> pictureProfiles = getPictureProfilesBasedOnConditions(column, - null, null); + List<PictureProfile> pictureProfiles = + mMqDatabaseUtils.getPictureProfilesBasedOnConditions(column, null, null); return pictureProfiles.stream() .map(PictureProfile::getPackageName) .distinct() @@ -561,7 +442,7 @@ public class MediaQualityService extends SystemService { if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { - notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -582,41 +463,18 @@ public class MediaQualityService extends SystemService { MediaQualityUtils.populateTempIdMap(mSoundProfileTempIdMap, id); String value = mSoundProfileTempIdMap.getValue(id); sp.setProfileId(value); - notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), + mMqManagerNotifier.notifyOnSoundProfileAdded(value, sp, Binder.getCallingUid(), Binder.getCallingPid()); return sp; } } - private void notifyHalOnSoundProfileChange(Long dbId, PersistableBundle params) { - // TODO: only notify HAL when the profile is active / being used - try { - mSpChangedListener.onSoundProfileChanged(convertToHalSoundProfile(dbId, params)); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to notify HAL on sound profile change.", e); - } - } - - private android.hardware.tv.mediaquality.SoundProfile convertToHalSoundProfile(Long id, - PersistableBundle params) { - SoundParameters soundParameters = new SoundParameters(); - soundParameters.soundParameters = - MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); - - android.hardware.tv.mediaquality.SoundProfile toReturn = - new android.hardware.tv.mediaquality.SoundProfile(); - toReturn.soundProfileId = id; - toReturn.parameters = soundParameters; - - return toReturn; - } - @GuardedBy("mSoundProfileLock") @Override public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) { Long dbId = mSoundProfileTempIdMap.getKey(id); if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { - notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -628,17 +486,12 @@ public class MediaQualityService extends SystemService { sp.getInputId(), sp.getParameters()); - SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); - db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - null, values); - notifyOnSoundProfileUpdated(mSoundProfileTempIdMap.getValue(dbId), - getSoundProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid()); - notifyHalOnSoundProfileChange(dbId, sp.getParameters()); + updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, sp.getParameters()); } } private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) { - SoundProfile fromDb = getSoundProfile(dbId); + SoundProfile fromDb = mMqDatabaseUtils.getSoundProfile(dbId); return fromDb.getProfileType() == sp.getProfileType() && fromDb.getPackageName().equals(sp.getPackageName()) && fromDb.getName().equals(sp.getName()) @@ -650,9 +503,10 @@ public class MediaQualityService extends SystemService { public void removeSoundProfile(String id, UserHandle user) { synchronized (mSoundProfileLock) { Long dbId = mSoundProfileTempIdMap.getKey(id); - SoundProfile toDelete = getSoundProfile(dbId); + SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); if (!hasPermissionToRemoveSoundProfile(toDelete)) { - notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(id, + SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } if (dbId != null) { @@ -663,13 +517,15 @@ public class MediaQualityService extends SystemService { selection, selectionArgs); if (result == 0) { - notifyOnSoundProfileError(id, SoundProfile.ERROR_INVALID_ARGUMENT, + mMqManagerNotifier.notifyOnSoundProfileError(id, + SoundProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); } - notifyOnSoundProfileRemoved(mSoundProfileTempIdMap.getValue(dbId), toDelete, + mMqManagerNotifier.notifyOnSoundProfileRemoved( + mSoundProfileTempIdMap.getValue(dbId), toDelete, Binder.getCallingUid(), Binder.getCallingPid()); mSoundProfileTempIdMap.remove(dbId); - notifyHalOnSoundProfileChange(dbId, null); + mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); } } } @@ -694,7 +550,7 @@ public class MediaQualityService extends SystemService { synchronized (mSoundProfileLock) { try ( - Cursor cursor = getCursorAfterQuerying( + Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, MediaQualityUtils.getMediaProfileColumns(includeParams), selection, selectionArguments) @@ -717,38 +573,12 @@ public class MediaQualityService extends SystemService { } } - private SoundProfile getSoundProfile(Long dbId) { - String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArguments = {Long.toString(dbId)}; - - try ( - Cursor cursor = getCursorAfterQuerying( - mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, - MediaQualityUtils.getMediaProfileColumns(false), selection, - selectionArguments) - ) { - int count = cursor.getCount(); - if (count == 0) { - return null; - } - if (count > 1) { - Log.wtf(TAG, TextUtils.formatSimple(String.valueOf(Locale.US), "%d entries " - + "found for id=%s in %s. Should only ever be 0 or 1.", count, - dbId, mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME)); - return null; - } - cursor.moveToFirst(); - return MediaQualityUtils.convertCursorToSoundProfileWithTempId( - cursor, mSoundProfileTempIdMap); - } - } - @GuardedBy("mSoundProfileLock") @Override public List<SoundProfile> getSoundProfilesByPackage( String packageName, Bundle options, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -757,7 +587,7 @@ public class MediaQualityService extends SystemService { options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; - return getSoundProfilesBasedOnConditions(MediaQualityUtils + return mMqDatabaseUtils.getSoundProfilesBasedOnConditions(MediaQualityUtils .getMediaProfileColumns(includeParams), selection, selectionArguments); } @@ -777,11 +607,13 @@ public class MediaQualityService extends SystemService { @Override public boolean setDefaultSoundProfile(String profileId, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - notifyOnSoundProfileError(profileId, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(profileId, + SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } - SoundProfile soundProfile = getSoundProfile(mSoundProfileTempIdMap.getKey(profileId)); + SoundProfile soundProfile = + mMqDatabaseUtils.getSoundProfile(mSoundProfileTempIdMap.getKey(profileId)); PersistableBundle params = soundProfile.getParameters(); try { @@ -805,13 +637,14 @@ public class MediaQualityService extends SystemService { @Override public List<String> getSoundProfilePackageNames(UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } String [] column = {BaseParameters.PARAMETER_NAME}; synchronized (mSoundProfileLock) { - List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column, + List<SoundProfile> soundProfiles = + mMqDatabaseUtils.getSoundProfilesBasedOnConditions(column, null, null); return soundProfiles.stream() .map(SoundProfile::getPackageName) @@ -851,177 +684,6 @@ public class MediaQualityService extends SystemService { mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; } - private Cursor getCursorAfterQuerying(String table, String[] columns, String selection, - String[] selectionArgs) { - SQLiteDatabase db = mMediaQualityDbHelper.getReadableDatabase(); - return db.query(table, columns, selection, selectionArgs, - /*groupBy=*/ null, /*having=*/ null, /*orderBy=*/ null); - } - - private List<PictureProfile> getPictureProfilesBasedOnConditions(String[] columns, - String selection, String[] selectionArguments) { - try ( - Cursor cursor = getCursorAfterQuerying( - mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, columns, selection, - selectionArguments) - ) { - List<PictureProfile> pictureProfiles = new ArrayList<>(); - while (cursor.moveToNext()) { - pictureProfiles.add(MediaQualityUtils.convertCursorToPictureProfileWithTempId( - cursor, mPictureProfileTempIdMap)); - } - return pictureProfiles; - } - } - - private List<SoundProfile> getSoundProfilesBasedOnConditions(String[] columns, - String selection, String[] selectionArguments) { - try ( - Cursor cursor = getCursorAfterQuerying( - mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, columns, selection, - selectionArguments) - ) { - List<SoundProfile> soundProfiles = new ArrayList<>(); - while (cursor.moveToNext()) { - soundProfiles.add(MediaQualityUtils.convertCursorToSoundProfileWithTempId( - cursor, mSoundProfileTempIdMap)); - } - return soundProfiles; - } - } - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - private @interface ProfileModes { - int ADD = 1; - int UPDATE = 2; - int REMOVE = 3; - int ERROR = 4; - } - - private void notifyOnPictureProfileAdded(String profileId, PictureProfile profile, - int uid, int pid) { - notifyPictureProfileHelper(ProfileModes.ADD, profileId, profile, null, uid, pid); - } - - private void notifyOnPictureProfileUpdated(String profileId, PictureProfile profile, - int uid, int pid) { - notifyPictureProfileHelper(ProfileModes.UPDATE, profileId, profile, null, uid, pid); - } - - private void notifyOnPictureProfileRemoved(String profileId, PictureProfile profile, - int uid, int pid) { - notifyPictureProfileHelper(ProfileModes.REMOVE, profileId, profile, null, uid, pid); - } - - private void notifyOnPictureProfileError(String profileId, int errorCode, - int uid, int pid) { - notifyPictureProfileHelper(ProfileModes.ERROR, profileId, null, errorCode, uid, pid); - } - - private void notifyPictureProfileHelper(int mode, String profileId, - PictureProfile profile, Integer errorCode, int uid, int pid) { - UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); - int n = userState.mPictureProfileCallbacks.beginBroadcast(); - - for (int i = 0; i < n; ++i) { - try { - IPictureProfileCallback callback = userState.mPictureProfileCallbacks - .getBroadcastItem(i); - Pair<Integer, Integer> pidUid = userState.mPictureProfileCallbackPidUidMap - .get(callback); - - if (pidUid.first == pid && pidUid.second == uid) { - if (mode == ProfileModes.ADD) { - userState.mPictureProfileCallbacks.getBroadcastItem(i) - .onPictureProfileAdded(profileId, profile); - } else if (mode == ProfileModes.UPDATE) { - userState.mPictureProfileCallbacks.getBroadcastItem(i) - .onPictureProfileUpdated(profileId, profile); - } else if (mode == ProfileModes.REMOVE) { - userState.mPictureProfileCallbacks.getBroadcastItem(i) - .onPictureProfileRemoved(profileId, profile); - } else if (mode == ProfileModes.ERROR) { - userState.mPictureProfileCallbacks.getBroadcastItem(i) - .onError(profileId, errorCode); - } - } - } catch (RemoteException e) { - if (mode == ProfileModes.ADD) { - Slog.e(TAG, "Failed to report added picture profile to callback", e); - } else if (mode == ProfileModes.UPDATE) { - Slog.e(TAG, "Failed to report updated picture profile to callback", e); - } else if (mode == ProfileModes.REMOVE) { - Slog.e(TAG, "Failed to report removed picture profile to callback", e); - } else if (mode == ProfileModes.ERROR) { - Slog.e(TAG, "Failed to report picture profile error to callback", e); - } - } - } - userState.mPictureProfileCallbacks.finishBroadcast(); - } - - private void notifyOnSoundProfileAdded(String profileId, SoundProfile profile, - int uid, int pid) { - notifySoundProfileHelper(ProfileModes.ADD, profileId, profile, null, uid, pid); - } - - private void notifyOnSoundProfileUpdated(String profileId, SoundProfile profile, - int uid, int pid) { - notifySoundProfileHelper(ProfileModes.UPDATE, profileId, profile, null, uid, pid); - } - - private void notifyOnSoundProfileRemoved(String profileId, SoundProfile profile, - int uid, int pid) { - notifySoundProfileHelper(ProfileModes.REMOVE, profileId, profile, null, uid, pid); - } - - private void notifyOnSoundProfileError(String profileId, int errorCode, int uid, int pid) { - notifySoundProfileHelper(ProfileModes.ERROR, profileId, null, errorCode, uid, pid); - } - - private void notifySoundProfileHelper(int mode, String profileId, - SoundProfile profile, Integer errorCode, int uid, int pid) { - UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); - int n = userState.mSoundProfileCallbacks.beginBroadcast(); - - for (int i = 0; i < n; ++i) { - try { - ISoundProfileCallback callback = userState.mSoundProfileCallbacks - .getBroadcastItem(i); - Pair<Integer, Integer> pidUid = userState.mSoundProfileCallbackPidUidMap - .get(callback); - - if (pidUid.first == pid && pidUid.second == uid) { - if (mode == ProfileModes.ADD) { - userState.mSoundProfileCallbacks.getBroadcastItem(i) - .onSoundProfileAdded(profileId, profile); - } else if (mode == ProfileModes.UPDATE) { - userState.mSoundProfileCallbacks.getBroadcastItem(i) - .onSoundProfileUpdated(profileId, profile); - } else if (mode == ProfileModes.REMOVE) { - userState.mSoundProfileCallbacks.getBroadcastItem(i) - .onSoundProfileRemoved(profileId, profile); - } else if (mode == ProfileModes.ERROR) { - userState.mSoundProfileCallbacks.getBroadcastItem(i) - .onError(profileId, errorCode); - } - } - } catch (RemoteException e) { - if (mode == ProfileModes.ADD) { - Slog.e(TAG, "Failed to report added sound profile to callback", e); - } else if (mode == ProfileModes.UPDATE) { - Slog.e(TAG, "Failed to report updated sound profile to callback", e); - } else if (mode == ProfileModes.REMOVE) { - Slog.e(TAG, "Failed to report removed sound profile to callback", e); - } else if (mode == ProfileModes.ERROR) { - Slog.e(TAG, "Failed to report sound profile error to callback", e); - } - } - } - userState.mSoundProfileCallbacks.finishBroadcast(); - } - //TODO: need lock here? @Override public void registerPictureProfileCallback(final IPictureProfileCallback callback) { @@ -1151,32 +813,19 @@ public class MediaQualityService extends SystemService { String name = MediaQualityUtils.getParameterName(pcHal.name); boolean isSupported = pcHal.isSupported; int type = pcHal.defaultValue == null ? 0 : pcHal.defaultValue.getTag() + 1; - Bundle bundle = convertToCaps(pcHal.range); + Bundle bundle = MediaQualityUtils.convertToCaps(type, pcHal.range); pcList.add(new ParameterCapability(name, isSupported, type, bundle)); } return pcList; } - private Bundle convertToCaps(ParameterRange range) { - Bundle bundle = new Bundle(); - if (range == null || range.numRange == null) { - return bundle; - } - bundle.putObject("INT_MIN_MAX", range.numRange.getIntMinMax()); - bundle.putObject("INT_VALUES_SUPPORTED", range.numRange.getIntValuesSupported()); - bundle.putObject("DOUBLE_MIN_MAX", range.numRange.getDoubleMinMax()); - bundle.putObject("DOUBLE_VALUES_SUPPORTED", range.numRange.getDoubleValuesSupported()); - bundle.putObject("LONG_MIN_MAX", range.numRange.getLongMinMax()); - bundle.putObject("LONG_VALUES_SUPPORTED", range.numRange.getLongValuesSupported()); - return bundle; - } - @GuardedBy("mPictureProfileLock") @Override public List<String> getPictureProfileAllowList(UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } String allowlist = mPictureProfileSharedPreference.getString(ALLOWLIST, null); @@ -1191,7 +840,8 @@ public class MediaQualityService extends SystemService { @Override public void setPictureProfileAllowList(List<String> packages, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } SharedPreferences.Editor editor = mPictureProfileSharedPreference.edit(); @@ -1203,7 +853,7 @@ public class MediaQualityService extends SystemService { @Override public List<String> getSoundProfileAllowList(UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } String allowlist = mSoundProfileSharedPreference.getString(ALLOWLIST, null); @@ -1218,7 +868,7 @@ public class MediaQualityService extends SystemService { @Override public void setSoundProfileAllowList(List<String> packages, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } SharedPreferences.Editor editor = mSoundProfileSharedPreference.edit(); @@ -1235,7 +885,8 @@ public class MediaQualityService extends SystemService { @Override public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } synchronized (mPictureProfileLock) { @@ -1272,7 +923,8 @@ public class MediaQualityService extends SystemService { @Override public void setSuperResolutionEnabled(boolean enabled, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnPictureProfileError(null, + PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } synchronized (mPictureProfileLock) { @@ -1309,7 +961,7 @@ public class MediaQualityService extends SystemService { @Override public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -1350,6 +1002,52 @@ public class MediaQualityService extends SystemService { } } + public void updatePictureProfileFromHal(Long dbId, PersistableBundle bundle) { + ContentValues values = MediaQualityUtils.getContentValues(dbId, + null, + null, + null, + null, + bundle); + + updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, bundle); + } + + public void updateDatabaseOnPictureProfileAndNotifyManagerAndHal(ContentValues values, + PersistableBundle bundle) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + db.replace(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + null, values); + Long dbId = values.getAsLong(BaseParameters.PARAMETER_ID); + mMqManagerNotifier.notifyOnPictureProfileUpdated(mPictureProfileTempIdMap.getValue(dbId), + mMqDatabaseUtils.getPictureProfile(dbId), Binder.getCallingUid(), + Binder.getCallingPid()); + mHalNotifier.notifyHalOnPictureProfileChange(dbId, bundle); + } + + public void updateSoundProfileFromHal(Long dbId, PersistableBundle bundle) { + ContentValues values = MediaQualityUtils.getContentValues(dbId, + null, + null, + null, + null, + bundle); + + updateDatabaseOnSoundProfileAndNotifyManagerAndHal(values, bundle); + } + + public void updateDatabaseOnSoundProfileAndNotifyManagerAndHal(ContentValues values, + PersistableBundle bundle) { + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); + db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + null, values); + Long dbId = values.getAsLong(BaseParameters.PARAMETER_ID); + mMqManagerNotifier.notifyOnSoundProfileUpdated(mSoundProfileTempIdMap.getValue(dbId), + mMqDatabaseUtils.getSoundProfile(dbId), Binder.getCallingUid(), + Binder.getCallingPid()); + mHalNotifier.notifyHalOnSoundProfileChange(dbId, bundle); + } + private class MediaQualityManagerPictureProfileCallbackList extends RemoteCallbackList<IPictureProfileCallback> { @Override @@ -1412,6 +1110,351 @@ public class MediaQualityService extends SystemService { return mUserStates.get(userId); } + private final class MqDatabaseUtils { + + MediaQualityDbHelper mMediaQualityDbHelper; + + private PictureProfile getPictureProfile(Long dbId) { + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArguments = {Long.toString(dbId)}; + + try ( + Cursor cursor = getCursorAfterQuerying( + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + MediaQualityUtils.getMediaProfileColumns(false), selection, + selectionArguments) + ) { + int count = cursor.getCount(); + if (count == 0) { + return null; + } + if (count > 1) { + Log.wtf(TAG, TextUtils.formatSimple(String.valueOf(Locale.US), "%d entries " + + "found for id=%d in %s. Should only ever be 0 or 1.", + count, dbId, mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME)); + return null; + } + cursor.moveToFirst(); + return MediaQualityUtils.convertCursorToPictureProfileWithTempId(cursor, + mPictureProfileTempIdMap); + } + } + + private List<PictureProfile> getPictureProfilesBasedOnConditions(String[] columns, + String selection, String[] selectionArguments) { + try ( + Cursor cursor = getCursorAfterQuerying( + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, columns, selection, + selectionArguments) + ) { + List<PictureProfile> pictureProfiles = new ArrayList<>(); + while (cursor.moveToNext()) { + pictureProfiles.add(MediaQualityUtils.convertCursorToPictureProfileWithTempId( + cursor, mPictureProfileTempIdMap)); + } + return pictureProfiles; + } + } + + private SoundProfile getSoundProfile(Long dbId) { + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArguments = {Long.toString(dbId)}; + + try ( + Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( + mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + MediaQualityUtils.getMediaProfileColumns(false), selection, + selectionArguments) + ) { + int count = cursor.getCount(); + if (count == 0) { + return null; + } + if (count > 1) { + Log.wtf(TAG, TextUtils.formatSimple(String.valueOf(Locale.US), "%d entries " + + "found for id=%s in %s. Should only ever be 0 or 1.", count, + dbId, mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME)); + return null; + } + cursor.moveToFirst(); + return MediaQualityUtils.convertCursorToSoundProfileWithTempId( + cursor, mSoundProfileTempIdMap); + } + } + + private List<SoundProfile> getSoundProfilesBasedOnConditions(String[] columns, + String selection, String[] selectionArguments) { + try ( + Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying( + mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, columns, selection, + selectionArguments) + ) { + List<SoundProfile> soundProfiles = new ArrayList<>(); + while (cursor.moveToNext()) { + soundProfiles.add(MediaQualityUtils.convertCursorToSoundProfileWithTempId( + cursor, mSoundProfileTempIdMap)); + } + return soundProfiles; + } + } + + private Cursor getCursorAfterQuerying(String table, String[] columns, String selection, + String[] selectionArgs) { + SQLiteDatabase db = mMediaQualityDbHelper.getReadableDatabase(); + return db.query(table, columns, selection, selectionArgs, + /*groupBy=*/ null, /*having=*/ null, /*orderBy=*/ null); + } + + private MqDatabaseUtils(Context context) { + mMediaQualityDbHelper = new MediaQualityDbHelper(context); + } + } + + private final class MqManagerNotifier { + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + private @interface ProfileModes { + int ADD = 1; + int UPDATE = 2; + int REMOVE = 3; + int ERROR = 4; + int PARAMETER_CAPABILITY_CHANGED = 5; + } + + private void notifyOnPictureProfileAdded(String profileId, PictureProfile profile, + int uid, int pid) { + notifyPictureProfileHelper(ProfileModes.ADD, profileId, profile, null, null, uid, pid); + } + + private void notifyOnPictureProfileUpdated(String profileId, PictureProfile profile, + int uid, int pid) { + notifyPictureProfileHelper(ProfileModes.UPDATE, profileId, profile, null, null, uid, + pid); + } + + private void notifyOnPictureProfileRemoved(String profileId, PictureProfile profile, + int uid, int pid) { + notifyPictureProfileHelper(ProfileModes.REMOVE, profileId, profile, null, null, uid, + pid); + } + + private void notifyOnPictureProfileError(String profileId, int errorCode, + int uid, int pid) { + notifyPictureProfileHelper(ProfileModes.ERROR, profileId, null, errorCode, null, uid, + pid); + } + + private void notifyOnPictureProfileParameterCapabilitiesChanged(Long profileId, + ParamCapability[] caps, int uid, int pid) { + String uuid = mPictureProfileTempIdMap.getValue(profileId); + List<ParameterCapability> paramCaps = new ArrayList<>(); + for (ParamCapability cap: caps) { + String name = MediaQualityUtils.getParameterName(cap.name); + boolean isSupported = cap.isSupported; + int type = cap.defaultValue == null ? 0 : cap.defaultValue.getTag() + 1; + Bundle bundle = MediaQualityUtils.convertToCaps(type, cap.range); + + paramCaps.add(new ParameterCapability(name, isSupported, type, bundle)); + } + notifyPictureProfileHelper(ProfileModes.PARAMETER_CAPABILITY_CHANGED, uuid, + null, null, paramCaps , uid, pid); + } + + private void notifyPictureProfileHelper(int mode, String profileId, + PictureProfile profile, Integer errorCode, + List<ParameterCapability> paramCaps, int uid, int pid) { + UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + int n = userState.mPictureProfileCallbacks.beginBroadcast(); + + for (int i = 0; i < n; ++i) { + try { + IPictureProfileCallback callback = userState.mPictureProfileCallbacks + .getBroadcastItem(i); + Pair<Integer, Integer> pidUid = userState.mPictureProfileCallbackPidUidMap + .get(callback); + + if (pidUid.first == pid && pidUid.second == uid) { + if (mode == ProfileModes.ADD) { + userState.mPictureProfileCallbacks.getBroadcastItem(i) + .onPictureProfileAdded(profileId, profile); + } else if (mode == ProfileModes.UPDATE) { + userState.mPictureProfileCallbacks.getBroadcastItem(i) + .onPictureProfileUpdated(profileId, profile); + } else if (mode == ProfileModes.REMOVE) { + userState.mPictureProfileCallbacks.getBroadcastItem(i) + .onPictureProfileRemoved(profileId, profile); + } else if (mode == ProfileModes.ERROR) { + userState.mPictureProfileCallbacks.getBroadcastItem(i) + .onError(profileId, errorCode); + } else if (mode == ProfileModes.PARAMETER_CAPABILITY_CHANGED) { + userState.mPictureProfileCallbacks.getBroadcastItem(i) + .onParameterCapabilitiesChanged(profileId, paramCaps); + } + } + } catch (RemoteException e) { + if (mode == ProfileModes.ADD) { + Slog.e(TAG, "Failed to report added picture profile to callback", e); + } else if (mode == ProfileModes.UPDATE) { + Slog.e(TAG, "Failed to report updated picture profile to callback", e); + } else if (mode == ProfileModes.REMOVE) { + Slog.e(TAG, "Failed to report removed picture profile to callback", e); + } else if (mode == ProfileModes.ERROR) { + Slog.e(TAG, "Failed to report picture profile error to callback", e); + } else if (mode == ProfileModes.PARAMETER_CAPABILITY_CHANGED) { + Slog.e(TAG, "Failed to report picture profile parameter capability change " + + "to callback", e); + } + } + } + userState.mPictureProfileCallbacks.finishBroadcast(); + } + + private void notifyOnSoundProfileAdded(String profileId, SoundProfile profile, + int uid, int pid) { + notifySoundProfileHelper(ProfileModes.ADD, profileId, profile, null, null, uid, pid); + } + + private void notifyOnSoundProfileUpdated(String profileId, SoundProfile profile, + int uid, int pid) { + notifySoundProfileHelper(ProfileModes.UPDATE, profileId, profile, null, null, uid, pid); + } + + private void notifyOnSoundProfileRemoved(String profileId, SoundProfile profile, + int uid, int pid) { + notifySoundProfileHelper(ProfileModes.REMOVE, profileId, profile, null, null, uid, pid); + } + + private void notifyOnSoundProfileError(String profileId, int errorCode, int uid, int pid) { + notifySoundProfileHelper(ProfileModes.ERROR, profileId, null, errorCode, null, uid, + pid); + } + + private void notifyOnSoundProfileParameterCapabilitiesChanged(Long profileId, + ParamCapability[] caps, int uid, int pid) { + String uuid = mSoundProfileTempIdMap.getValue(profileId); + List<ParameterCapability> paramCaps = new ArrayList<>(); + for (ParamCapability cap: caps) { + String name = MediaQualityUtils.getParameterName(cap.name); + boolean isSupported = cap.isSupported; + int type = cap.defaultValue == null ? 0 : cap.defaultValue.getTag() + 1; + Bundle bundle = MediaQualityUtils.convertToCaps(type, cap.range); + + paramCaps.add(new ParameterCapability(name, isSupported, type, bundle)); + } + notifySoundProfileHelper(ProfileModes.PARAMETER_CAPABILITY_CHANGED, uuid, + null, null, paramCaps , uid, pid); + } + + private void notifySoundProfileHelper(int mode, String profileId, + SoundProfile profile, Integer errorCode, + List<ParameterCapability> paramCaps, int uid, int pid) { + UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + int n = userState.mSoundProfileCallbacks.beginBroadcast(); + + for (int i = 0; i < n; ++i) { + try { + ISoundProfileCallback callback = userState.mSoundProfileCallbacks + .getBroadcastItem(i); + Pair<Integer, Integer> pidUid = userState.mSoundProfileCallbackPidUidMap + .get(callback); + + if (pidUid.first == pid && pidUid.second == uid) { + if (mode == ProfileModes.ADD) { + userState.mSoundProfileCallbacks.getBroadcastItem(i) + .onSoundProfileAdded(profileId, profile); + } else if (mode == ProfileModes.UPDATE) { + userState.mSoundProfileCallbacks.getBroadcastItem(i) + .onSoundProfileUpdated(profileId, profile); + } else if (mode == ProfileModes.REMOVE) { + userState.mSoundProfileCallbacks.getBroadcastItem(i) + .onSoundProfileRemoved(profileId, profile); + } else if (mode == ProfileModes.ERROR) { + userState.mSoundProfileCallbacks.getBroadcastItem(i) + .onError(profileId, errorCode); + } else if (mode == ProfileModes.PARAMETER_CAPABILITY_CHANGED) { + userState.mSoundProfileCallbacks.getBroadcastItem(i) + .onParameterCapabilitiesChanged(profileId, paramCaps); + } + } + } catch (RemoteException e) { + if (mode == ProfileModes.ADD) { + Slog.e(TAG, "Failed to report added sound profile to callback", e); + } else if (mode == ProfileModes.UPDATE) { + Slog.e(TAG, "Failed to report updated sound profile to callback", e); + } else if (mode == ProfileModes.REMOVE) { + Slog.e(TAG, "Failed to report removed sound profile to callback", e); + } else if (mode == ProfileModes.ERROR) { + Slog.e(TAG, "Failed to report sound profile error to callback", e); + } else if (mode == ProfileModes.PARAMETER_CAPABILITY_CHANGED) { + Slog.e(TAG, "Failed to report sound profile parameter capability change " + + "to callback", e); + } + } + } + userState.mSoundProfileCallbacks.finishBroadcast(); + } + + private MqManagerNotifier() { + + } + } + + private final class HalNotifier { + + private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) { + // TODO: only notify HAL when the profile is active / being used + try { + mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId, + params)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify HAL on picture profile change.", e); + } + } + + private android.hardware.tv.mediaquality.PictureProfile convertToHalPictureProfile(Long id, + PersistableBundle params) { + PictureParameters pictureParameters = new PictureParameters(); + pictureParameters.pictureParameters = + MediaQualityUtils.convertPersistableBundleToPictureParameterList( + params); + + android.hardware.tv.mediaquality.PictureProfile toReturn = + new android.hardware.tv.mediaquality.PictureProfile(); + toReturn.pictureProfileId = id; + toReturn.parameters = pictureParameters; + + return toReturn; + } + + private void notifyHalOnSoundProfileChange(Long dbId, PersistableBundle params) { + // TODO: only notify HAL when the profile is active / being used + try { + mSpChangedListener.onSoundProfileChanged(convertToHalSoundProfile(dbId, params)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify HAL on sound profile change.", e); + } + } + + private android.hardware.tv.mediaquality.SoundProfile convertToHalSoundProfile(Long id, + PersistableBundle params) { + SoundParameters soundParameters = new SoundParameters(); + soundParameters.soundParameters = + MediaQualityUtils.convertPersistableBundleToSoundParameterList(params); + + android.hardware.tv.mediaquality.SoundProfile toReturn = + new android.hardware.tv.mediaquality.SoundProfile(); + toReturn.soundProfileId = id; + toReturn.parameters = soundParameters; + + return toReturn; + } + + private HalNotifier() { + + } + } + private final class AmbientBacklightCallbackRecord implements IBinder.DeathRecipient { final String mPackageName; final IAmbientBacklightCallback mCallback; @@ -1443,6 +1486,125 @@ public class MediaQualityService extends SystemService { } } + private final class PictureProfileAdjustmentListenerImpl extends + IPictureProfileAdjustmentListener.Stub { + MqDatabaseUtils mMqDatabaseUtils; + MqManagerNotifier mMqManagerNotifier; + HalNotifier mHalNotifier; + + @Override + public void onPictureProfileAdjusted( + android.hardware.tv.mediaquality.PictureProfile pictureProfile) + throws RemoteException { + Long dbId = pictureProfile.pictureProfileId; + if (dbId != null) { + updatePictureProfileFromHal(dbId, + MediaQualityUtils.convertPictureParameterListToPersistableBundle( + pictureProfile.parameters.pictureParameters)); + } + } + + @Override + public void onParamCapabilityChanged(long pictureProfileId, ParamCapability[] caps) + throws RemoteException { + mMqManagerNotifier.notifyOnPictureProfileParameterCapabilitiesChanged( + pictureProfileId, caps, Binder.getCallingUid(), Binder.getCallingPid()); + } + + @Override + public void onVendorParamCapabilityChanged(long pictureProfileId, + VendorParamCapability[] caps) throws RemoteException { + // TODO + } + + @Override + public void requestPictureParameters(long pictureProfileId) throws RemoteException { + PictureProfile profile = mMqDatabaseUtils.getPictureProfile(pictureProfileId); + if (profile != null) { + mHalNotifier.notifyHalOnPictureProfileChange(pictureProfileId, + profile.getParameters()); + } + } + + @Override + public void onStreamStatusChanged(long pictureProfileId, byte status) + throws RemoteException { + // TODO + } + + @Override + public int getInterfaceVersion() throws RemoteException { + return 0; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return null; + } + + private PictureProfileAdjustmentListenerImpl(Context context) { + mMqDatabaseUtils = new MqDatabaseUtils(context); + mMqManagerNotifier = new MqManagerNotifier(); + mHalNotifier = new HalNotifier(); + } + } + + private final class SoundProfileAdjustmentListenerImpl extends + ISoundProfileAdjustmentListener.Stub { + MqDatabaseUtils mMqDatabaseUtils; + MqManagerNotifier mMqManagerNotifier; + HalNotifier mHalNotifier; + + @Override + public void onSoundProfileAdjusted( + android.hardware.tv.mediaquality.SoundProfile soundProfile) throws RemoteException { + Long dbId = soundProfile.soundProfileId; + if (dbId != null) { + updateSoundProfileFromHal(dbId, + MediaQualityUtils.convertSoundParameterListToPersistableBundle( + soundProfile.parameters.soundParameters)); + } + } + + @Override + public void onParamCapabilityChanged(long soundProfileId, ParamCapability[] caps) + throws RemoteException { + mMqManagerNotifier.notifyOnSoundProfileParameterCapabilitiesChanged( + soundProfileId, caps, Binder.getCallingUid(), Binder.getCallingPid()); + } + + @Override + public void onVendorParamCapabilityChanged(long pictureProfileId, + VendorParamCapability[] caps) throws RemoteException { + // TODO + } + + @Override + public void requestSoundParameters(long soundProfileId) throws RemoteException { + SoundProfile profile = mMqDatabaseUtils.getSoundProfile(soundProfileId); + if (profile != null) { + mHalNotifier.notifyHalOnSoundProfileChange(soundProfileId, + profile.getParameters()); + } + } + + @Override + public int getInterfaceVersion() throws RemoteException { + return 0; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return null; + } + + private SoundProfileAdjustmentListenerImpl(Context context) { + mMqDatabaseUtils = new MqDatabaseUtils(context); + mMqManagerNotifier = new MqManagerNotifier(); + mHalNotifier = new HalNotifier(); + } + } + private final class HalAmbientBacklightCallback extends android.hardware.tv.mediaquality.IMediaQualityCallback.Stub { private final Object mLock = new Object(); diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java index 03780413f8a1..d021a27afb02 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -20,16 +20,20 @@ import android.content.ContentValues; import android.database.Cursor; import android.hardware.tv.mediaquality.DolbyAudioProcessing; import android.hardware.tv.mediaquality.DtsVirtualX; +import android.hardware.tv.mediaquality.ParameterDefaultValue; import android.hardware.tv.mediaquality.ParameterName; +import android.hardware.tv.mediaquality.ParameterRange; import android.hardware.tv.mediaquality.PictureParameter; import android.hardware.tv.mediaquality.SoundParameter; import android.media.quality.MediaQualityContract.BaseParameters; import android.media.quality.MediaQualityContract.PictureQuality; import android.media.quality.MediaQualityContract.SoundQuality; +import android.media.quality.ParameterCapability; import android.media.quality.PictureProfile; import android.media.quality.PictureProfileHandle; import android.media.quality.SoundProfile; import android.media.quality.SoundProfileHandle; +import android.os.Bundle; import android.os.PersistableBundle; import android.util.Log; @@ -1511,6 +1515,30 @@ public final class MediaQualityUtils { return parameterNameMap.get(pn); } + /** + * Convert ParameterRange to a Bundle. + */ + public static Bundle convertToCaps(int type, ParameterRange range) { + Bundle bundle = new Bundle(); + if (range == null || range.numRange == null) { + return bundle; + } + type -= 1; + if (type == ParameterDefaultValue.intDefault) { + bundle.putObject(ParameterCapability.CAPABILITY_MIN, range.numRange.getIntMinMax()[0]); + bundle.putObject(ParameterCapability.CAPABILITY_MAX, range.numRange.getIntMinMax()[1]); + } else if (type == ParameterDefaultValue.doubleDefault) { + bundle.putObject(ParameterCapability.CAPABILITY_MIN, + range.numRange.getDoubleMinMax()[0]); + bundle.putObject(ParameterCapability.CAPABILITY_MAX, + range.numRange.getDoubleMinMax()[1]); + } else if (type == ParameterDefaultValue.longDefault) { + bundle.putObject(ParameterCapability.CAPABILITY_MIN, range.numRange.getLongMinMax()[0]); + bundle.putObject(ParameterCapability.CAPABILITY_MAX, range.numRange.getLongMinMax()[1]); + } + return bundle; + } + private static String getTempId(BiMap<Long, String> map, Cursor cursor) { int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_ID); Long dbId = colIndex != -1 ? cursor.getLong(colIndex) : null; diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index a80b1b2dd9e8..fab19b6b8201 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -284,11 +284,4 @@ public interface StatusBarManagerInternal { /** Passes through the given shell commands to SystemUI */ void passThroughShellCommand(String[] args, FileDescriptor fd); - - /** - * Set whether the display should have a navigation bar. - * - * TODO(b/390591772): Refactor this method - */ - void setHasNavigationBar(int displayId, boolean hasNavigationBar); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index c546388e4499..da9d01675984 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -1011,23 +1011,6 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D public void passThroughShellCommand(String[] args, FileDescriptor fd) { StatusBarManagerService.this.passThroughShellCommand(args, fd); } - - @Override - public void setHasNavigationBar(int displayId, boolean hasNavigationBar) { - if (isVisibleBackgroundUserOnDisplay(displayId)) { - if (SPEW) { - Slog.d(TAG, "Skipping setHasNavigationBar for visible background user " - + mUserManagerInternal.getUserAssignedToDisplay(displayId)); - } - return; - } - IStatusBar bar = mBar; - if (bar != null) { - try { - bar.setHasNavigationBar(displayId, hasNavigationBar); - } catch (RemoteException ex) {} - } - } }; private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() { diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 5090ed04fee1..10f591cfd379 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -756,20 +756,7 @@ public class DisplayPolicy { return; } - final boolean hasNavigationBar = mDisplayContent.isSystemDecorationsSupported(); - if (mHasNavigationBar == hasNavigationBar) { - return; - } - - mHasNavigationBar = hasNavigationBar; - mHandler.post( - () -> { - final int displayId = getDisplayId(); - StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); - if (statusBar != null) { - statusBar.setHasNavigationBar(displayId, mHasNavigationBar); - } - }); + mHasNavigationBar = mDisplayContent.isSystemDecorationsSupported(); } public boolean hasStatusBar() { diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index 28aa6eff911b..539fc123720e 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -122,7 +122,7 @@ class DisplayWindowSettings { } void setIgnoreOrientationRequest(@NonNull DisplayContent displayContent, - boolean ignoreOrientationRequest) { + @Nullable Boolean ignoreOrientationRequest) { final DisplayInfo displayInfo = displayContent.getDisplayInfo(); final SettingsProvider.SettingsEntry overrideSettings = mSettingsProvider.getOverrideSettings(displayInfo); diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index aaae160084a9..e63107cdc720 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -570,10 +570,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr private boolean restoreFromCachedStateIfPossible(@NonNull ITaskFragmentOrganizer organizer, int pid, int uid, @NonNull Bundle outSavedState) { - if (!Flags.aeBackStackRestore()) { - return false; - } - TaskFragmentOrganizerState cachedState = null; int i = mCachedTaskFragmentOrganizerStates.size() - 1; for (; i >= 0; i--) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 7f1924005b2f..5ead3554fda3 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4342,6 +4342,23 @@ public class WindowManagerService extends IWindowManager.Stub } } + @Nullable + Boolean resetIgnoreOrientationRequest(int displayId) { + synchronized (mGlobalLock) { + final DisplayContent display = mRoot.getDisplayContent(displayId); + if (display == null) { + return null; + } + display.mHasSetIgnoreOrientationRequest = false; + // Clear existing override settings. + mDisplayWindowSettings.setIgnoreOrientationRequest(display, + null /* ignoreOrientationRequest */); + // Reload from settings in case there is built-in config. + mDisplayWindowSettings.applyRotationSettingsToDisplayLocked(display); + return display.getIgnoreOrientationRequest(); + } + } + /** * Controls whether ignore orientation request logic in {@link DisplayArea} is disabled * at runtime and how to optionally map some requested orientations to others. diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 20b01d0dc618..6534c42c7572 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -578,6 +578,18 @@ public class WindowManagerShellCommand extends ShellCommand { displayId = Integer.parseInt(getNextArgRequired()); arg = getNextArgRequired(); } + if ("reset".equals(arg)) { + final Boolean result = mInternal.resetIgnoreOrientationRequest(displayId); + if (result != null) { + pw.println("Reset ignoreOrientationRequest to " + result + " for displayId=" + + displayId); + return 0; + } else { + getErrPrintWriter().println( + "Unable to reset ignoreOrientationRequest for displayId=" + displayId); + return -1; + } + } final boolean ignoreOrientationRequest; switch (arg) { @@ -590,7 +602,7 @@ public class WindowManagerShellCommand extends ShellCommand { ignoreOrientationRequest = false; break; default: - getErrPrintWriter().println("Error: expecting true, 1, false, 0, but we " + getErrPrintWriter().println("Error: expecting true, 1, false, 0, reset, but we " + "get " + arg); return -1; } @@ -1525,7 +1537,7 @@ public class WindowManagerShellCommand extends ShellCommand { mInterface.setFixedToUserRotation(displayId, IWindowManager.FIXED_TO_USER_ROTATION_DEFAULT); // set-ignore-orientation-request - mInterface.setIgnoreOrientationRequest(displayId, false /* ignoreOrientationRequest */); + mInternal.resetIgnoreOrientationRequest(displayId); // set-letterbox-style resetLetterboxStyle(); @@ -1568,7 +1580,7 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" fixed-to-user-rotation [-d DISPLAY_ID] [enabled|disabled|default"); pw.println(" |enabled_if_no_auto_rotation]"); pw.println(" Print or set rotating display for app requested orientation."); - pw.println(" set-ignore-orientation-request [-d DISPLAY_ID] [true|1|false|0]"); + pw.println(" set-ignore-orientation-request [-d DISPLAY_ID] [reset|true|1|false|0]"); pw.println(" get-ignore-orientation-request [-d DISPLAY_ID] "); pw.println(" If app requested orientation should be ignored."); pw.println(" set-sandbox-display-apis [true|1|false|0]"); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java new file mode 100644 index 000000000000..f0334598bd30 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -0,0 +1,119 @@ +/* + * 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.accessibility.autoclick; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper; +import android.view.View; +import android.view.WindowManager; +import android.widget.LinearLayout; + +import com.android.internal.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Test cases for {@link AutoclickTypePanel}. */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class AutoclickTypePanelTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public TestableContext mTestableContext = + new TestableContext(getInstrumentation().getContext()); + + private AutoclickTypePanel mAutoclickTypePanel; + @Mock private WindowManager mMockWindowManager; + private LinearLayout mLeftClickButton; + private LinearLayout mRightClickButton; + private LinearLayout mDoubleClickButton; + private LinearLayout mDragButton; + private LinearLayout mScrollButton; + + @Before + public void setUp() { + mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); + + mAutoclickTypePanel = new AutoclickTypePanel(mTestableContext, mMockWindowManager); + View contentView = mAutoclickTypePanel.getContentViewForTesting(); + mLeftClickButton = contentView.findViewById(R.id.accessibility_autoclick_left_click_layout); + mRightClickButton = + contentView.findViewById(R.id.accessibility_autoclick_right_click_layout); + mDoubleClickButton = + contentView.findViewById(R.id.accessibility_autoclick_double_click_layout); + mScrollButton = contentView.findViewById(R.id.accessibility_autoclick_scroll_layout); + mDragButton = contentView.findViewById(R.id.accessibility_autoclick_drag_layout); + } + + @Test + public void AutoclickTypePanel_initialState_expandedFalse() { + assertThat(mAutoclickTypePanel.getExpansionStateForTesting()).isFalse(); + } + + @Test + public void AutoclickTypePanel_initialState_correctButtonVisibility() { + // On initialization, only left button is visible. + assertThat(mLeftClickButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mRightClickButton.getVisibility()).isEqualTo(View.GONE); + assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.GONE); + assertThat(mDragButton.getVisibility()).isEqualTo(View.GONE); + assertThat(mScrollButton.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void togglePanelExpansion_onClick_expandedTrue() { + // On clicking left click button, the panel is expanded and all buttons are visible. + mLeftClickButton.callOnClick(); + + assertThat(mAutoclickTypePanel.getExpansionStateForTesting()).isTrue(); + assertThat(mLeftClickButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mRightClickButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mDragButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mScrollButton.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void togglePanelExpansion_onClickAgain_expandedFalse() { + // By first click, the panel is expanded. + mLeftClickButton.callOnClick(); + assertThat(mAutoclickTypePanel.getExpansionStateForTesting()).isTrue(); + + // Clicks any button in the expanded state, the panel is expected to collapse + // with only the clicked button visible. + mScrollButton.callOnClick(); + + assertThat(mAutoclickTypePanel.getExpansionStateForTesting()).isFalse(); + assertThat(mScrollButton.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mRightClickButton.getVisibility()).isEqualTo(View.GONE); + assertThat(mLeftClickButton.getVisibility()).isEqualTo(View.GONE); + assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.GONE); + assertThat(mDragButton.getVisibility()).isEqualTo(View.GONE); + } +} |